Nesting is not Sass nesting
The ampersand is a selector, not a string. The discipline problem hasn't changed at all.
Native CSS nesting has been in all three engines for a while now, and I’ve been using it on a project since the spring.
The first thing I did, entirely automatically, was this:
.card {
padding: 1rem;
&__title { font-size: 1.25rem; } /* nope */
}
Which is how I’ve written BEM in Sass for about ten years, and which does not work, and the reason it doesn’t work is the single most important thing to understand about the native version.
& is a selector, not a string
In Sass, the whole thing is text processing. & is substituted with the parent selector as a string, so &__title becomes the literal text .card__title and the compiler emits that. It’s a clever trick and it’s why BEM and Sass nesting became a pair.
In CSS, nesting is not a preprocessor. & is a genuine selector that refers to the enclosing rule’s subject, resolved by the browser at parse time. There’s no string to concatenate to. &__title isn’t a selector that means anything, so it’s invalid.
Which means the single most common use of nesting in the Sass world, building up class names from fragments, is simply not available natively and never will be. If you write your CSS that way, and a great many people do, nesting isn’t a like-for-like replacement for you at all.
Sass nesting is text substitution. Native nesting is a real selector reference. Everything that differs between them follows from that one sentence.
I’d add that this is arguably the better design. The Sass version means you can’t search your codebase for .card__title and find where it’s defined, which has cost me real time, and which I’d stopped noticing because I was used to it.
And the specificity differs
The subtler one, and the one that will produce a genuinely confusing afternoon.
& behaves like :is(), which means it takes the specificity of the most specific selector in the enclosing list.
.button, #cta {
& .icon { fill: red; }
}
/* the nested rule has the specificity of #cta, even when it matches .button */
In Sass the output would have been flattened to .button .icon, #cta .icon, and the first of those has ordinary class specificity. Natively, both matches carry the id’s weight.
Nobody hits this until they have a selector list with something heavy in it, and then it’s baffling, because the rule you can see looks like a class selector and is beating things it shouldn’t. If you’re mixing ids into selector lists you have other problems, and this is a reason to notice them.
What it’s good for
The two uses that were always the best part of nesting, and which now cost nothing:
States next to the thing. &:hover, &:focus-visible, &[aria-expanded="true"], &:disabled sitting inside the rule they modify, rather than 11 lines further down where somebody will eventually separate them.
Media queries inside the rule. This is the one I’d have paid for.
.card {
display: block;
padding: 1rem;
&:hover { border-color: var(--accent); }
@media (min-width: 40em) {
display: grid;
grid-template-columns: 8rem 1fr;
}
}
The responsive behavior of a component is now in the component, rather than in a media query block at the bottom of the file that lists 20 unrelated selectors. That block has been the worst part of every stylesheet I’ve written and it’s gone.
Note also that you don’t need & before a declaration inside the media query, and that plain element selectors nest directly in current browsers, though older versions from about a year ago required a symbol first.
The discipline problem hasn’t changed
Here’s the bit I’d most want somebody to hear.
I wrote a post in 2015 about why I stopped nesting selectors five deep in Sass. The reasons were: the output is a long descendant selector with high specificity, the source doesn’t show you what the final selector is so you can’t tell what you’ve written, and it couples your CSS to your DOM structure so a markup change breaks styles in a file nobody opened.
Every one of those is still true natively. The syntax changed, the failure mode didn’t. Three levels of nesting produces .page .card .card__body a whether a preprocessor made it or the browser did, and it’s equally horrible either way.
So the rule I settled on nearly a decade ago still stands, and I’d offer it as the whole practical takeaway: nest for state and context, never for structure. A pseudo-class, an attribute state, a media query, a :has() condition. Not a descendant hierarchy.
One level, occasionally two. If you’re at three, you’ve drawn a picture of your HTML in your stylesheet, and one of them is going to change.
Does this kill Sass
Not on its own, and it’s another leg off.
Nesting was one of about five reasons I used a preprocessor. Variables went native in 2016 and are better than the Sass version for anything that changes at runtime. The cascade problem I used to solve with careful ordering is now @layer. Color functions are increasingly native. What’s left is loops, math at build time, generating things from a map, and file concatenation.
Which is a real list and it’s a much shorter one than it was, and on two projects this year the answer was “nothing on that list, actually.” So I’m using less of it every year and I haven’t removed it from anything, which is probably the most honest summary I can give of every tooling question I’ve written about.
If you take one thing
& is a selector, not a string, so &__title doesn’t work and nothing will make it work. Everything else you know transfers.
And nest one level. It was good advice in 2015 and the new syntax hasn’t made descendant selectors any nicer.