Getting Started with CSS Nesting: What Shipped, the Relaxed Parsing Gap, and the Fallback
You are writing CSS nesting in production and the fallback that covers the gap between what shipped first and what shipped later. Native CSS nesting with the & token is Baseline newly available as of 2024, but the relaxed parsing behaviour that lets you write an element selector without & shipped later, and older engines that implemented the earlier spec text will reject that rule as invalid. The production fallback is a Lightning CSS or PostCSS build step that compiles nested CSS to flat selectors, or hand-written flat selectors for the patterns the earlier spec rejects. This page tells you what shipped, what fails, and how to keep the CSS nesting production fallback preprocessor strategy honest without sacrificing the syntax you already use.
What Shipped: Native CSS Nesting with the & Token
CSS nesting went from proposal to shipped in every engine between 2023 and 2024. The spec that shipped first, CSS Nesting Module Level 1, requires the & token for almost every nested rule. You write the parent selector explicitly, and the nested rule inherits the parent’s context. The Baseline status for this initial version is newly available since 2023, and the later revision that relaxed the parsing rules moved the whole feature to widely available on 2024-12-19, with Chrome 120, Edge 120, Firefox 117, and Safari 17.2 all shipping the relaxed behaviour.
The gap is real and dates from a specification revision. The CSS Working Group changed the grammar after the first implementations landed, removing the requirement that a nested selector must start with &. MDN browser-compat-data tracks the change as separate entries: the strict &-requirement versus the relaxed element selector nesting. An engine that shipped the 2023 text will parse .card & and & .title correctly, but it will throw away a rule that starts with a bare .title inside the block. That is not a bug in your code; it is a version boundary inside the spec itself.
The & Token and the Nesting Selector
The & token is the nesting selector. It represents the parent rule’s selector, and it can appear anywhere in a compound selector, not just at the front. &:hover works, .theme-dark & works, and &.active works. The & token is also what makes the nested rule’s specificity predictable: the nested rule’s specificity is computed as if the parent selector were repeated at the point where & appears. That means & .child has the same specificity as the flat .parent .child, and &:hover has the same specificity as .parent:hover.
The key restriction in the shipped spec is that a nested rule may not start with a type selector or a universal selector when the & token is absent. You cannot write p { .highlight { } } and expect it to mean .p .highlight; the earlier parsers reject it because the nested selector does not begin with &. The relaxed parsing behaviour, which shipped later, allows that form and treats it as a descendant combinator, exactly as a preprocessor would have done for years.
The Relaxed Parsing Gap in Practice
Here is the working nested rule with the & token, the form every modern engine accepts:
.card {
& .title {
color: #333;
font-weight: 600;
}
&:hover {
border-color: #999;
}
}
That compiles to the flat selectors .card .title and .card:hover with no ambiguity. Now the same rule written with an element selector without &, which the relaxed parsing behaviour allows:
.card {
.title {
color: #333;
font-weight: 600;
}
:hover {
border-color: #999;
}
}
In an engine that shipped the earlier spec text, the .title rule is invalid and is dropped entirely. The :hover rule is also invalid because a pseudo-class without a preceding compound selector is not a valid nested selector under the strict grammar. The rule block survives, but the declarations inside it are gone. Your card loses its title colour and its hover border, silently, with no console warning.
The compiled flat output that works everywhere is what the preprocessor fallback produces:
.card .title {
color: #333;
font-weight: 600;
}
.card:hover {
border-color: #999;
}
That is the entire production story: write with &, compile with Lightning CSS or PostCSS, and ship flat CSS. If you skip the build step, you must write the flat selectors by hand for every rule that uses a nested element selector without &.
CSS Nesting Relaxed Parsing Interop and the Version Boundary
The interop picture is not a simple yes/no. Every engine now supports the relaxed parsing behaviour, but the version boundary means you cannot assume it. A browser that updated to Safari 17.2 or Firefox 117 gets the relaxed grammar; a browser locked to Safari 17.0 or Firefox 116 does not. That is not a niche case. iOS Safari on unsupported devices stays at an older version indefinitely, and Android WebView inside an app that does not update its engine can sit years behind. No reliable survey pins the exact percentage of users on device-locked browsers who will reject your relaxed-parse CSS, and you will not see the error because the rule is invalid and dropped.
The Interop 2024 project explicitly targeted CSS nesting as an interop pain point, and the WPT subtests for relaxed parsing were among the failures that drove the revision. The spec text now says that a nested rule may omit the leading & when the selector begins with a type, universal, or pseudo-class, and the parser must treat it as a descendant combinator. That is the behaviour Sass and Less have had for a decade, and it is the behaviour you expect when you read nested CSS as a human.
Token Rules You Must Remember
The token rules are the difference between code that parses and code that vanishes. A nested selector that starts with a letter, an element selector like p, div, or span, requires the &. Without it, the strict parser sees a type selector at the start of a nested rule and rejects it. A nested selector that starts with a class, an ID, an attribute, or a pseudo-class is fine without & under the relaxed grammar, but only if the engine ships the relaxed parsing behaviour. The safest pattern is to write & everywhere except for the cases where you know the build step will compile it, because the build step does not care which grammar your source uses.
Concatenation is another trap. In Sass, &__item produces a class name by string interpolation. In native CSS nesting, &__item is a compound selector that means the parent selector followed by the class __item, which is almost certainly not what you wanted. CSS nesting cannot concatenate strings to form selectors; the & token is a selector, not a string, and the grammar has no interpolation mechanism. That is a hard difference from preprocessor nesting, and it is the reason many design systems keep Sass for component class generation and use native nesting only for structural rules.
CSS Nesting vs Sass Nesting Differences
The differences are not cosmetic. Sass nesting is more permissive: it allows & to appear anywhere, it allows element selectors without & at any depth, and it has string interpolation for class names. Native CSS nesting has the & token requirement, the relaxed parsing behaviour with its version boundary, and no interpolation. Sass also allows parent selectors to be repeated and combined with ... selectors; CSS nesting has no such feature. If you port a Sass file to native nesting, you must manually rewrite every & concatenation and every element selector without & so it starts with & or a class. The compiled output is identical, but the source is not.
Less follows the same pattern as Sass, with & for the parent and permissive parsing. The practical consequence is that a codebase that migrated from Sass to native nesting and then hit the strict-parser boundary needs a fallback, not a reinterpretation. The fallback is a build step that compiles the nested CSS to flat selectors, which is exactly what Lightning CSS and PostCSS do with the nesting plugin. That is the CSS nesting production fallback preprocessor in action: you write the nested syntax, the build step flattens it, and the browser never sees the nested form.
CSS Nesting @supports Fallback Pattern That Actually Works
The @supports fallback pattern is the only way to use native nesting without a build step. You guard the nested rule with a feature query that tests for the & token, and you provide the flat fallback outside the guard. The query is @supports (selector(&)) { … }, and it evaluates to true in any engine that supports the & token in a selector context. The relaxed parsing behaviour is not separately testable; if the engine supports selector(&), it almost certainly supports the relaxed grammar, but the only safe assumption is that the & token form works.
Here is the pattern in practice:
.card .title {
color: #333;
font-weight: 600;
}
.card:hover {
border-color: #999;
}
@supports (selector(&)) {
.card {
& .title {
color: #333;
font-weight: 600;
}
&:hover {
border-color: #999;
}
}
}
That compiles to the same flat selectors in the fallback and the nested form in the guard. The browser that supports & uses the nested version; the browser that does not uses the flat version. The cost is duplication: every rule appears twice, once flat and once nested. For a large stylesheet, that is maintenance overhead and a source of drift when one copy is edited and the other is not. The failure mode is a rule that exists only in the nested branch, so a legacy browser gets no styling at all for that component.
The @supports guard is not a silver bullet. It cannot detect the relaxed parsing behaviour separately from the & token behaviour, because the two shipped in the same engine versions. A browser that supports selector(&) but not element selector nesting without & will pass the guard and then drop the nested .title rule inside the block. That is the exact interop gap the relaxed parsing revision was meant to close, and it is still present in any engine that shipped the earlier spec text. The guard protects against engines that lack & entirely, not against engines that have & but lack relaxed parsing.
The acceptable fallback for production is therefore not the @supports guard alone; it is the build step. Lightning CSS and PostCSS both parse native nesting and output flat selectors. The build step removes the version boundary entirely because the browser never sees the nested form. The @supports guard is the right tool for a small site with no build pipeline, or for a progressive enhancement where the nested form is a bonus and the flat form is the baseline. For a design system with thousands of rules, the build step is the only sane option.
The PostCSS and Lightning CSS Build Step as the Fallback
PostCSS with the postcss-nesting plugin is the standard toolchain for CSS nesting in production. It reads your source CSS, parses the nested rules, and emits flat selectors that any browser can parse. The plugin implements the CSS Nesting Module Level 1 spec, including the relaxed parsing behaviour, and it preserves the order of your rules and the cascade. Lightning CSS is the faster alternative: it is a Rust-based parser, transformer, and minifier that targets modern browser syntax without transpiling to older forms. It handles nesting, custom properties, and the rest of modern CSS in one pass, and its output is smaller than PostCSS output because it does not add legacy prefixes unless you ask for them.
The build step does not change how you write CSS. You write the nested syntax with & and element selectors without &, the build step compiles it to flat selectors, and the browser receives the flat form. The compiled output is exactly what a hand-written flat stylesheet would be, with the same specificity and the same cascade order. The only difference is that you wrote it nested, and the build step did the unnesting for you.
What the Build Step Does Not Do
The build step does not fix the relaxed parsing gap in your source; it avoids it. If you write a nested rule that starts with an element selector without &, the build step compiles it to the correct descendant combinator. If you write a rule that uses & for concatenation, the build step will either reject it as invalid or produce a compound selector that is not what you intended. The build step is a parser, not a magic rewriter. It follows the same grammar as the browser, and it will flag the same errors a strict parser would flag, except that the error appears at build time rather than silently in a user’s browser.
A common mistake is nesting more than one level deep without & and expecting concatenation. Consider this:
.card {
.title {
strong {
color: #444;
}
}
}
The relaxed parsing behaviour treats each nested selector as a descendant of the parent, so this means .card .title strong. That is correct. But if you write .card { &__title { } }, the build step sees a compound selector &__title, which is the parent selector followed by the class __title. If the parent is .card, that is .card__title only by accident; the grammar does not know you wanted a BEM-style concatenation, and the result is a selector that matches an element with both classes card and __title, which almost never exists. The correct pattern is a separate class or a custom property, not concatenation.
When the Build Step Is the Wrong Answer
There is one situation where the build step is the wrong answer: a single-file stylesheet that is edited by hand and deployed directly, with no tooling. If you are writing CSS for a static site and you do not have a build step, the @supports guard is the fallback, and it requires duplication. If you are using a CSS-in-JS library that injects styles at runtime, the nesting is handled by the library’s own parser, and the build step is irrelevant. The audience is the front-end developer writing daily CSS, and for that developer the build step is the default, not the exception.
The failure case for the build step is a developer who forgets to run it. The source is nested, the deployed file is nested, and the browser that does not support the nested grammar drops the rules. The symptom is a page that looks broken in one browser and fine in another, with no console error and no network failure. The solution is a test that checks the deployed CSS for a nested & token and fails the build if it finds one. That is a ten-line script, and it is the difference between a silent regression and a caught bug.
Hand-Written Flat Selectors for the Patterns the Earlier Spec Rejects
If you cannot use a build step and you cannot rely on the @supports guard, the fallback is hand-written flat selectors. This is the preprocessor technique that native nesting replaces, and it still works everywhere. For every nested rule, you write the full selector as a flat compound, repeating the parent selector as many times as the nesting depth requires. The specificity is identical because the flat selector has the same compound sequence as the nested rule’s computed specificity.
Here is the pattern for a two-level nesting:
.card .title .highlight {
color: #e90;
}
.card .title .highlight:hover {
color: #c70;
}
That is what the build step would produce from this nested source:
.card {
.title {
.highlight {
color: #e90;
&:hover {
color: #c70;
}
}
}
}
The flat form is verbose, and it repeats .card .title in every rule. For a component with ten nested states, the flat form is three times longer than the nested form. That verbosity is the price of the fallback, and it is the reason the build step exists. The flat form is also the form that a preprocessor would have compiled to a decade ago, so it is battle-tested and understood by every CSS developer.
The hand-written flat form has one advantage over the @supports guard: there is no duplication. You write the flat selector once, and it works in every browser. The @supports guard requires writing the same rule twice, once flat and once nested, and the two copies can drift. The hand-written flat form has no drift because there is no second copy. The cost is that you lose the nesting syntax entirely, and you are back to the manual repetition that native nesting was supposed to replace.
The Common Mistake That Breaks the Flat Fallback
Omitting & before a nested selector that begins with a letter is the common mistake that breaks the fallback. In a hand-written flat stylesheet, there is no &, so the mistake is different: you forget to include the parent selector in the flat compound. You write .title .highlight instead of .card .title .highlight, and the rule matches elements outside the card. The failure is not a parse error; it is a specificity error that styles the wrong elements. That is harder to debug than a dropped rule, because the styling appears in the wrong place and you have to trace which rule is the culprit.
Another common mistake is nesting more than one level deep without &, expecting concatenation where a descendant combinator is generated instead. In the flat form, that mistake becomes a missing parent in the selector chain. The rule still matches, but it matches too broadly. The fix is to write the full chain every time, and to resist the urge to abbreviate the parent. A lint rule that flags any selector containing a descendant combinator that does not start with the component class can catch this automatically.
What the Spec Revision Changed and Why MDN Data Matters
The CSS Working Group revised the nesting spec in a way that changed the grammar, not just the recommendations. The original CSS Nesting Module Level 1 required a leading & for every nested rule except a few specific cases. The revision, published as the same spec URL, https://www.w3.org/TR/css-nesting-1/, removed that requirement and made the leading & optional when the nested selector begins with a type, universal, or pseudo-class. The reason was interop: the strict grammar made the common preprocessor pattern of .parent { .child { } } invalid, and every developer who migrated from Sass hit the error on the first day. The relaxation aligns native nesting with the preprocessor behaviour that has been stable for years.
MDN browser-compat-data tracks this as two entries. The first entry records native CSS nesting with the & token requirement, and the second records the relaxed parsing behaviour. The Baseline status for the combined feature is widely available since 2024-12-19, but the compat data keeps the two entries separate because an engine that shipped the strict grammar can still be in the field. When you look up CSS nesting on MDN, the compat table shows a separate row for relaxed parsing, and it lists the first browser version that supports it. That row is the one you check before you decide whether to use element selector nesting without &.
The Interop 2024 project, which coordinates browser vendors to fix the most impactful interop gaps, listed CSS nesting as a target. The WPT subtests for relaxed parsing were failing in multiple engines at the start of the project, and the fixes landed in the browser releases that shipped the relaxed grammar. The Interop scoreboard was the forcing function; without it, the revision might have waited longer. The practical takeaway is that a feature’s Baseline status is a rolling window, not a permanent truth, and the revision moved the goalposts for what counts as supported.
The Specificity of Nested Rules Under the Relaxed Grammar
The specificity of a nested rule is computed from its full selector after the & token is replaced by the parent selector. Under the relaxed grammar, a nested rule without & is treated as if it had an implicit & at the front, so the specificity is the same as the flat descendant selector. That is a change from the strict grammar, where a nested rule had to include & and could place it anywhere in the compound, changing the specificity accordingly. The relaxed grammar simplifies the mental model: every nested rule is a descendant of the parent, and the specificity is the sum of the parent’s specificity and the nested selector’s specificity.
That consistency is why the relaxed parsing behaviour is easier to reason about in production. You do not have to calculate whether a rule with & in the middle has higher or lower specificity than the flat form; it is always the same as the flat form. The only exception is a rule that uses & more than once, which repeats the parent selector and increases specificity. The rule for that is the same in both grammars: each occurrence of & contributes the parent’s specificity.
The Design-System Author’s Checklist for CSS Nesting in Production
If you maintain a design system, the checklist for CSS nesting in production has four items. First, decide whether your build step compiles nesting away. If it does, write with the relaxed grammar freely, using element selectors without & where they are natural. The build step protects you from the version boundary, and the compiled output is identical to a flat stylesheet. Second, if you ship native nesting without a build step, require & in your lint rules. That is the only syntax that every engine that supports nesting at all will accept. The relaxed grammar is a bonus, not a baseline, and your lint rule should reflect that.
Third, test the compiled output for & tokens. A build step that fails to compile leaves the nested form in the output, and the browser drops the rules. A CI check that greps the deployed CSS for & and fails on it is a cheap insurance policy. Fourth, document the difference between native nesting and preprocessor nesting for your team. The most common error is trying to concatenate class names with &__item, which works in Sass and fails in CSS nesting. A short section in your style guide that shows the invalid pattern and the correct flat alternative will save more debugging time than any other documentation you write.
The failure mode for a design system is a component that uses element selector nesting without &, ships to a browser that has the strict parser, and loses its styles. The symptom is a visual regression that only appears on older iOS Safari or Android WebView, and it is reproducible only on those devices. The fix is either the build step or the & requirement. The build step is the right choice for a design system with a build pipeline, which is most of them. The & requirement is the right choice for a design system that ships raw CSS as a package, which is less common but still exists.
What This Topic Suits and Who Should Skip It
Native CSS nesting suits the front-end developer who controls the build pipeline and wants to write less repetitive CSS. It suits the design-system author who needs a syntax that matches the cascade and specificity, not a preprocessor’s string manipulation. It suits the technical writer who needs to explain the difference between the strict and relaxed grammar without hand-waving. Board this train.
Skip it if you are happy with Sass and have no plan to migrate. Sass nesting is more permissive, has interpolation, and is stable; switching to native nesting buys you nothing except the removal of a build dependency, and you still need a build step for the fallback. Skip it if you ship raw CSS with no tooling and cannot accept the @supports guard’s duplication. For you, hand-written flat selectors remain the best tool. Skip it if you are learning CSS from zero; start with the MDN CSS first-steps guide and return only after you can write a flat selector in your sleep.
The @supports guard cannot distinguish the strict grammar from the relaxed grammar, so a browser that passes the guard can still drop your element-selector nesting, and the only reliable protection is a build step that flattens the nesting before the browser ever sees it.