Skip to main content
TF
By Rohit V.8 min readArticle

Native CSS Nesting Explained, Without a Preprocessor

TF
ToolsFuel Team
Web development tools & tips
CSS stylesheet code on a laptop screen

Photo by Unsplash on Unsplash

Short Answer

> Quick answer: CSS nests natively now, no build step. Support landed in Chrome 120, Edge 120, Safari 17.2 and Firefox 117, so it's been shipping across every current browser since the end of 2023 and sits comfortably in Baseline. Write a selector inside another selector and it resolves against the parent, exactly like it reads.

The `&` is optional in most positions since the relaxed grammar shipped, so `.card { p { color: red } }` works. You still need `&` when the nested part attaches to the parent rather than descending from it, so `&:hover` and `&.active` need it and `:hover` on its own means something different. The one thing that catches people is specificity: `&` behaves like `:is()`, so it takes the weight of the heaviest selector in the list, not the one that matched.

The Ampersand Rule, Stated Once

Every confusing thing about nesting comes back to one question. Does the nested selector describe a descendant, or does it describe the parent element itself in some other state?

Descendant means no `&` is needed. Inside `.card`, writing `h2 { }` targets an `h2` anywhere inside the card, and the browser inserts the descendant combinator for you. That's identical to writing `.card h2` at the top level.


Attaching to the parent means `&` is required, and it goes where the parent's selector text would go. `&:hover` compiles to `.card:hover`, the card itself being hovered. Write `:hover` without the ampersand and you've said `.card *:hover`, which is any descendant being hovered. Both are valid CSS and they do completely different things, which is why this is the single most common nesting bug.


The same applies to compound classes. `&.active` is the card when it also carries `.active`, while `.active` alone is a descendant with that class. And `&` can sit on the right too: `.dark &` inside `.card` produces `.dark .card`, which is the clean way to write theme overrides without leaving the component block.


I've settled on writing `&` even where it's optional. It costs one character and it removes the entire category of question, which is worth more than the brevity.

The Specificity Surprise

This one is genuinely surprising and it isn't a browser quirk, it's in the spec.

The `&` selector computes its specificity the way `:is()` does, which means it takes the weight of the most specific selector in the parent list, regardless of which one actually matched the element in front of you.


So if the parent rule is `.card, #featured`, then `&` inside it carries the specificity of `#featured`, an ID, even when the element being styled is a plain `.card`. A later rule with a single class will lose to it, and nothing in the stylesheet visibly explains why.


The practical rule that falls out of this is to avoid mixing wildly different specificities in a single selector list that you then nest inside. Keep the parent list uniform, all classes or all elements, and the nested rules behave the way they look. Our
specificity walkthrough covers how the numbers are counted if that part is fuzzy.

Worth knowing too: this applies to `&` specifically, not to bare descendant nesting. `.card { p { } }` has the specificity of `.card p`, which is what you'd expect.

What Native Nesting Does Not Do

Sass users hit this on day one. You cannot build selector names by concatenation. In Sass, `.btn { &--large { } }` produces `.btn--large`, and native CSS has no equivalent. The `&` is a real selector reference, not a string that gets glued to the next characters, so `&--large` is a syntax error rather than a BEM shortcut.

If your naming scheme depends on that, native nesting won't replace your build step for it. Write `.btn--large` out in full, which is more searchable anyway. Grepping for a class name and actually finding it is an underrated property of a codebase.


Nesting also doesn't give you variables, mixins, loops or functions. Custom properties cover the variable case natively and cascade properly, which is better than a preprocessor variable for anything theme related, but there's no native `@mixin`.


One more constraint: a nested rule must start with something that can't be mistaken for a property. That's why the relaxed syntax was needed at all. Older implementations required `&` or a symbol at the start so the parser could tell a rule from a declaration, and the relaxed grammar removed that restriction by letting the parser look further ahead. On current browsers you don't have to think about it.


The
MDN nesting reference lists the exact positions `&` is allowed in.

Nesting Media Queries and Container Queries

This is the part that makes nesting worth adopting even if you never nest a single selector.

You can put an `@media` block inside a rule, and everything in it applies to that rule's selector. So a component's responsive behaviour lives inside the component block rather than in a media query three hundred lines down that repeats the selector. The rule and its breakpoint sit together, which is the actual maintenance win.


The same works for `@container`, and it pairs particularly well, because a container query is already scoped to a component by design. Nesting the query inside the component's own rule removes the last bit of duplication. Our
container queries guide goes through the sizing rules.

`@supports` nests too, which is handy for progressive enhancement that you'd otherwise have to write twice.


The tradeoff is depth. Once a rule contains a media query which contains a nested selector which contains a state, you're four levels in and the resulting selector is no longer obvious from reading any one line. Two levels is comfortable, three is the point to stop and ask whether the component wants splitting.

How Deep to Nest in Practice

Deep nesting was a Sass problem long before it was a CSS one, and the failure mode hasn't changed. Every level adds specificity, and specificity you didn't intend is what turns a stylesheet into a pile of overrides.

Two levels covers most real components. The component's own rule, and one level of children or states inside it. At that depth the compiled selector is still short enough to hold in your head, and a later single class rule can still override it without a fight.


Three levels is a smell rather than an error. It usually means the markup has a wrapper that deserves its own class, and giving it one flattens the CSS and makes the HTML more readable at the same time.


Watch out for the copy paste path too. Nesting makes it easy to move a block somewhere else in the file, and the block's meaning changes silently because its parent changed. A flat selector carries its own context, a nested one borrows it.


The other habit worth keeping is ordering. Put the parent's own declarations first, then the state rules using `&`, then nested children, then any media or container queries last. Nothing enforces that order and the browser doesn't care, but a stylesheet where every block follows the same shape is far quicker to scan than one where declarations and nested rules are interleaved. I found that mattered more than the nesting depth itself once a file passed a few hundred lines, because the eye learns where to look.


One practical note on tooling. Autoprefixer and most minifiers handle native nesting fine now, but older toolchains that predate the feature can flatten it incorrectly or choke on the relaxed syntax. If nested rules vanish in a production build but work in dev, that's the first thing I'd check rather than the CSS itself.


If you want to sanity check what a nested block compiles to, the fastest thing is the browser's own devtools, which shows the resolved selector in the styles panel rather than the nested source. For quick formatting of the output there's the
CSS gradient generator and the rest of our tools, but for nesting specifically, devtools is the honest answer.

Frequently Asked Questions

Do I still need Sass if CSS nests natively?

Only for what nesting does not cover, mainly mixins, loops and selector concatenation like &--large. For plain nesting, media queries inside rules and variables, native CSS and custom properties handle it without a build step.

Is the ampersand required in CSS nesting?

Not for descendants since the relaxed syntax shipped, so .card { p { } } works. It is required when the nested selector attaches to the parent, such as &:hover or &.active, because :hover alone means a descendant instead.

Why is my nested rule beating a later rule?

The & takes the specificity of the heaviest selector in the parent list, the same way :is() does. A parent list mixing an ID with a class gives every nested rule ID level weight. Our [specificity guide](/blog/css-specificity-explained-how-it-works) explains the counting.

Which browsers support native CSS nesting?

Chrome 120, Edge 120, Safari 17.2 and Firefox 117 and later, shipping since late 2023. It is part of Baseline now, so no fallback is needed for current browsers.

Can I nest media queries inside a rule?

Yes, and it is the strongest reason to adopt nesting. An @media block inside a rule applies to that rule's selector, so a component keeps its breakpoints next to its base styles instead of in a separate section.

Try ToolsFuel

23+ free online tools for developers, designers, and everyone. No signup required.

Browse All Tools