← blog
CSSJuly 9, 2026 · 4 min read

You kept Sass for one reason. Native CSS nesting just ended it.

CSS has supported nesting natively since 2023 — pseudo-classes, pseudo-elements, and @media queries nested inside the rules they belong to. No compiler required.

Parsa Jiravand · Frontend engineer · building bestpractic
You kept Sass for one reason. Native CSS nesting just ended it.

There's a project on every developer's machine that has Sass installed for one reason: &:hover {}. Not @mixin. Not @each. Just the nesting. The variables long since became --custom-properties. The only thing still justifying node_modules/sass is the ability to write child selectors inside parent rules.

CSS added that natively in 2023. It shipped in Chrome 112, Firefox 117, and Safari 16.5 — every major browser released in the last two years. The compiler is not earning its spot anymore.

The classic pattern — component styles scoped to a block, with states and modifiers nested inside:

SCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.card { padding: 1.5rem; border-radius: 0.5rem; background: var(--surface); &:hover { background: var(--surface-hover); } &__title { font-size: 1.125rem; font-weight: 600; } &--featured { border: 2px solid var(--accent); } }

The output is flat, specificity-controlled CSS. The source is organized by component. That's the trade Sass nesting has always offered — and native CSS now offers the same deal.

CSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.card { padding: 1.5rem; border-radius: 0.5rem; background: var(--surface); &:hover { background: var(--surface-hover); } & .card__title { font-size: 1.125rem; font-weight: 600; } &.card--featured { border: 2px solid var(--accent); } }

Two differences are worth noticing. First: pseudo-classes work exactly as in Sass — &:hover resolves to .card:hover with no extra syntax. Second: descendant selectors require an explicit & followed by a space. & .card__title becomes .card .card__title. This is where native nesting differs from BEM's __/-- convention: in native CSS, & is a selector reference, not a string concatenation operator.

If you're using BEM naming heavily, &__foo becomes & .block__foo. The compiled output is identical; the source is slightly more explicit about what's happening.

Runs right in your browser — poke at it and watch the concept react live.

This is the feature that earns native nesting a permanent spot in how I write stylesheets.

Before, responsive CSS meant jumping between two locations in the same file — the base rule near the top and the @media block somewhere below it, often hundreds of lines away:

CSS
1
2
3
4
5
6
7
8
9
10
11
12
13
.hero { font-size: 1.5rem; padding: 2rem; } /* ... 200 lines later ... */ @media (min-width: 768px) { .hero { font-size: 2.5rem; padding: 4rem; } }

With native nesting, the responsive rules live with the component they modify:

CSS
1
2
3
4
5
6
7
8
9
.hero { font-size: 1.5rem; padding: 2rem; @media (min-width: 768px) { font-size: 2.5rem; padding: 4rem; } }

Reading an unfamiliar stylesheet gets faster. Instead of tracing a component across multiple @media blocks, all its states are in one place. You see what it looks like at every breakpoint without scrolling past unrelated rules.

Sass could nest @media queries this way too — but only by compiling them back out to separate blocks. Native CSS parses nested @media directly. No output transformation, no source map needed to trace back.

In Sass, & is a string that gets concatenated. &__child produces .card__child. That's why BEM's double-underscore syntax worked so cleanly: the ampersand literally builds the class name character by character.

In native CSS, & is a selector. It means "the selector that contains this rule," and the browser uses it for matching, not text-building. So &__child doesn't produce .card__child — the parser sees it as & (a valid selector ref) followed by __child (an unknown pseudo-class-like token) and ignores the rule silently.

The fix is mechanical: everywhere you have &__element, write & .block__element. Everywhere you have &--modifier, write &.block--modifier (no space, modifier is on the same element). The specificity and the rendered output are identical. The migration is a find-and-replace once you know the pattern to look for.

Chrome 112 (April 2023), Firefox 117 (August 2023), Safari 16.5 (May 2023). That's Baseline 2023: cross-browser in any browser released in the past two years.

For projects that still need to cover older browsers, native nesting degrades safely — unknown nested rules are silently skipped, so the outer rule's styles still apply:

CSS
1
2
3
4
5
6
7
8
9
/* Both browsers parse the outer rule */ .card { padding: 1.5rem; /* Older browsers skip this; newer ones apply it */ &:hover { background: var(--surface-hover); } }

The progressive-enhancement story is the same one container queries, scroll-driven animations, and @layer all tell: declare it, get the enhancement in modern browsers, get the safe fallback in older ones.

Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.

Open the project with Sass installed. Check whether you use anything beyond nesting: @mixin, @each, @function, @use. If the answer is no — just nesting — you have a straightforward migration. Rename .scss files to .css, change &__child patterns to & .child, remove the build step.

The loop is usually shorter than expected. Two file renames, a find-and-replace, one fewer devDependency. The styles work the same way; the pipeline gets simpler.

What Sass feature are you genuinely still relying on? Because the list gets shorter every year. Nesting was the last one most projects could point to — and it just moved to the platform.


Thanks for reading! Let's stay connected:

Originally published on dev.to