Sass nesting → native CSS nesting: same syntax, no compiler
CSS Nesting (the & nesting selector) shipped
natively in all evergreen browsers by late 2023. The syntax is nearly
identical to Sass — nest a selector inside a parent rule, and &
refers to that parent. The difference: no build step. Tailwind v4 leans
on this hard — its engine (Lightning CSS) desugars nesting for you everywhere
it appears, so a custom @utility can co-locate its hover state,
child styles, and pseudo-elements in ONE block instead of scattering flat rules.
| aspect | Sass nesting | native CSS nesting |
|---|---|---|
| build step | required — sass/dart-sass compiles to CSS |
none — the browser parses & flattens nesting directly |
the & selector |
refers to the parent selector (Sass interpolation) | identical — refers to the parent rule's matched element |
| specificity | exact flat selector (.card .title) |
flat, but parent is wrapped in :is() in reversed/complex cases (takes max specificity) |
| in Tailwind v4 | use only via a separate Sass pipeline (not integrated) | first-class — works inside @utility, @layer, plain CSS in <style type="text/tailwindcss"> |
BEM suffix &__elem |
fully supported, idiomatic | supported in modern browsers (late spec addition) — works, but historically spotty |
1 · @utility card-nest — one utility, four nested rules (gold-check)
This is the whole lesson in one block. The custom utility
card-nest owns its &:hover lift, its
& .nk-title / & .nk-body descendants, and its
&::before star + &::after accent bar. None
of these are separate flat selectors — they are nested inside the utility.
Hover the card to feel the &:hover rule fire.
&:hover.
The gold-check asserts all three nesting channels compiled:
::before carries the star, .nk-title carries
font-weight: 700, and a literal .card-nest:hover
rule exists in the live stylesheet. Before Tailwind's JIT runs, all three
are false — they flip together, which is the proof.
2 · the & selector — every position it can appear
& is short for "the parent rule's selector". It can lead
(most common), trail (to reverse context), repeat (sibling chains), or sit
inside compound selectors. The one place it cannot reach is a
pseudo-element of a reversed parent — see the gotchas panel.
| you write (nested) | desugars to | meaning |
|---|---|---|
&:hover { … } |
.card:hover { … } |
style the element ITSELF in a state |
& .title { … } |
.card .title { … } |
descendant (note the space) — implicit if you omit & |
& > .child { … } |
.card > .child { … } |
direct child only |
&::before { … } |
.card::before { … } |
generate/box pseudo-element of the parent |
.featured & { … } |
.featured .card { … } |
trailing & reverses context (parent as descendant) |
& + & { … } |
.card + .card { … } |
repeat & — adjacent sibling of same kind |
bare :hover { … } |
.card *:hover { … } |
GOTCHA — a bare pseudo (no &) targets DESCENDANTS, not self |
Why this matters in Tailwind: inside @utility card-nest,
writing :hover { … } instead of &:hover { … }
would style hovered descendants of every card — silently wrong. Always
prefix state pseudo-classes with &.
3 · where nesting works inside Tailwind v4
Tailwind v4 processes your whole CSS source through Lightning CSS, so
nesting desugars anywhere a real CSS rule can live. The only place it does
not belong is @theme (that block holds variables only).
| context | nesting supported? | emitted selector example |
|---|---|---|
@utility name { &:hover } |
✅ yes — idiomatic for custom utilities | .name:hover (variant-friendly: hover: stacks) |
@layer components { .x { & .y } } |
✅ yes | .x .y inside the components layer |
plain rule .tag-pill { & > b } |
✅ yes — see the live demo below | .tag-pill > b |
@theme { &:hover } |
❌ no — @theme is variables only |
— (move the rule to @utility / @layer) |
inside @custom-variant |
⚠️ variants define selector lists, not nested rules — use & as a placeholder there (different meaning) |
@custom-variant pointer-coarse (&:hover) |
Plain-CSS nesting in action — .tag-pill (not a utility, just a
class) with nested & > b and & > span:
4 · browser support — Baseline widely available
Native CSS Nesting reached Baseline "Widely available" in December 2023. Tailwind v4 targets this baseline, so nesting is safe to ship without a fallback in any v4-supported browser.
| browser | min version | shipped |
|---|---|---|
| Chrome / Edge | 112+ | April 2023 |
| Safari (desktop + iOS) | 16.5+ | May 2023 |
| Firefox | 117+ | August 2023 |
| Samsung Internet | 22+ | 2023 |
Lightning CSS (Tailwind v4's engine) desugars nesting regardless of target — so even the @tailwindcss/browser@4 CDN flattens it before the browser sees it. | ||
intent → pattern
| intent | pattern | why |
|---|---|---|
| co-locate a utility's hover/focus/active | @utility btn { &:hover { … } &:focus-visible { … } } |
one block, no scattered flat selectors; variants still stack |
| style a child of a custom utility | & .label { font-weight: 700 } |
descendant via space; & > .label for direct child |
| add a decorative pseudo-element | &::before { content: "★" } |
must use & — bare ::before is invalid nested |
| style self only in an ancestor context | .dark & { … } |
trailing & reverses context (no dark: variant needed) |
| scope a media query to one selector | & { @media (width < 600px) { … } } |
at-rules nest too; the properties are implicitly wrapped in & |
| avoid the silent bare-pseudo bug | write &:hover, never bare :hover |
bare :hover styles descendants — the #1 nesting footgun |