DEV Community

Cover image for You kept Sass for one reason. Native CSS nesting just ended it.
Parsa Jiravand
Parsa Jiravand

Posted on • Edited on

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.

What you've been writing in Sass

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

.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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

The same thing in native CSS

.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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

🎮 Try it yourself

▶️ Open the interactive playground →

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

Media queries nested inside their rules

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:

.hero {
  font-size: 1.5rem;
  padding: 2rem;
}

/* ... 200 lines later ... */

@media (min-width: 768px) {
  .hero {
    font-size: 2.5rem;
    padding: 4rem;
  }
}
Enter fullscreen mode Exit fullscreen mode

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

.hero {
  font-size: 1.5rem;
  padding: 2rem;

  @media (min-width: 768px) {
    font-size: 2.5rem;
    padding: 4rem;
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

The one gotcha — & is not a string

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.

Browser support — the honest picture

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:

/* Both browsers parse the outer rule */
.card {
  padding: 1.5rem;

  /* Older browsers skip this; newer ones apply it */
  &:hover {
    background: var(--surface-hover);
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

🧠 Test yourself

Think it clicked? Take the 6-question quiz →

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

What to do next

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:

Top comments (4)

Collapse
 
nazar-boyko profile image
Nazar Boyko

Quick check on the "specificity and rendered output are identical" part, because I think there's a wrinkle hiding in the BEM migration. Sass compiles &__title into the flat selector .card__title, a single class. Native & .card__title compiles to .card .card__title, which is two classes deep and only matches when the element actually sits inside a .card. For normal BEM markup the rendering usually comes out the same, but the specificity doubles, so any override that used to beat .card__title can quietly start losing after the find and replace. Or am I missing a rule in how & desugars that flattens this? If not, it might deserve a warning box in the migration section, since it's the kind of thing that shows up three components later rather than in the diff.

Collapse
 
parsajiravand profile image
Parsa Jiravand

You're not missing anything—that's an important distinction.

Sass's &title performs selector concatenation and generates .cardtitle, whereas native CSS nesting doesn't support that kind of concatenation. Writing & .card_title produces .card .card_title, which has different specificity and only matches descendants of .card.

So you're absolutely right that a straightforward find-and-replace isn't equivalent and can lead to unexpected override behavior later on. That's exactly the kind of subtle issue that's easy to miss during a migration because everything may appear to work until another rule relies on the original specificity.

Thanks for calling this out. It definitely deserves a note in the migration section so readers don't assume the two patterns are interchangeable.

Collapse
 
frank_signorini profile image
Frank

Native CSS nesting is a game changer, but I'm curious how it handles complex selectors like &:hover. Will this finally make me ditch Sass? Would love to hear your thoughts on this.

Collapse
 
parsajiravand profile image
Parsa Jiravand

Great question! Native CSS nesting handles selectors like &:hover, &:focus, &::before, and even more complex combinations just fine—they work very similarly to how they do in Sass.

As for replacing Sass, I think it depends on how you're using it. If you're mainly relying on Sass for nesting, modern CSS has largely caught up, and native nesting is a great reason to simplify your toolchain. But if your project depends heavily on features like mixins, functions, loops, or advanced module organization, Sass still provides capabilities that native CSS doesn't fully replace.

For many new projects, though, I'd seriously consider using native CSS nesting first and only adding Sass if I find I genuinely need those extra features.