Type something to search...
Native CSS Nesting: Do You Still Need Sass?

Native CSS Nesting: Do You Still Need Sass?

Ask a developer why they installed Sass, and the most likely answer is "nesting". Being able to write a component's styles inside one block, with hover states and child elements tucked underneath, made stylesheets easier to read and organize. Variables, mixins, and functions were nice extras, but nesting was the feature people didn't want to live without.

Native CSS nesting is now supported in every major browser. You can write nested rules in a plain .css file, with no build step, and the browser understands them. So it's a fair question: do you still need Sass? This article explains how native nesting works, where it behaves differently from Sass, and what Sass still offers that CSS doesn't.

Native Nesting Basics

Nesting lets you place a style rule inside another style rule. The inner rule's selector is relative to the outer one.

.card {
  padding: 1.5rem;
  border-radius: 12px;
  background: #ffffff;

  .card-title {
    font-size: 1.25rem;
    margin-block-end: 0.5rem;
  }

  &:hover {
    box-shadow: 0 8px 24px rgb(15 23 42 / 0.12);
  }
}

The browser treats this as:

.card {
  padding: 1.5rem;
  border-radius: 12px;
  background: #ffffff;
}

.card .card-title {
  font-size: 1.25rem;
  margin-block-end: 0.5rem;
}

.card:hover {
  box-shadow: 0 8px 24px rgb(15 23 42 / 0.12);
}

The & nesting selector

The & symbol represents the parent rule's selector. When you use it, you control exactly where the parent goes:

.button {
  &:hover {
  } /* .button:hover */
  &.is-active {
  } /* .button.is-active */
  & + & {
  } /* .button + .button */
  .toolbar & {
  } /* .toolbar .button */
  &::before {
  } /* .button::before */
}

When a nested selector has no &, the browser inserts one with a descendant combinator, so .card-title inside .card means .card .card-title.

Combinators

Nested rules can start with a combinator:

.list {
  > li {
    padding-block: 0.5rem;
  } /* .list > li */
  + .list {
    margin-block-start: 2rem;
  } /* .list + .list */
  ~ p {
    color: #475569;
  } /* .list ~ p */
}

Starting with an element selector

Early versions of the spec required nested selectors to start with a symbol, so you couldn't write p { } directly inside a rule without & p. That restriction was removed (the "relaxed" nesting syntax), and all current major browsers now accept bare element selectors:

.article {
  h2 {
    font-size: 1.75rem;
  }
  p {
    line-height: 1.7;
  }
}

If you need to support the small number of browsers that shipped the earlier version, write & h2 and & p instead. It's always valid.

Nesting at-rules

Media queries, container queries, @supports, and @layer can be nested directly inside a rule. The properties inside apply to the parent selector:

.sidebar {
  display: none;

  @media (min-width: 900px) {
    display: block;
    width: 280px;
  }

  @supports (container-type: inline-size) {
    container-type: inline-size;
  }
}

This is one of the nicest parts of native nesting. A component's responsive behavior lives right next to its base styles.

How Native Nesting Differs from Sass

The syntax looks almost identical, but there are important differences under the hood.

1. No string concatenation with &

This is the biggest one. In Sass, & is text substitution, so you can build BEM-style class names:

// Sass
.card {
  &__title {
    font-weight: 700;
  }
  &--featured {
    border: 2px solid #38bdf8;
  }
}
// Compiles to .card__title and .card--featured

In native CSS, & represents a selector, not a string. &__title is treated as & followed by a type selector __title, which isn't what you want. Native CSS has no way to glue suffixes onto class names. If your codebase depends heavily on BEM concatenation, you'll need to write the full class names:

.card {
  .card__title {
    font-weight: 700;
  } /* .card .card__title */
  &.card--featured {
    border: 2px solid #38bdf8;
  } /* .card.card--featured */
}

Note that the nested .card__title selector now includes a descendant combinator, which Sass's version didn't. For BEM, where elements are always inside the block, that's harmless, but it slightly raises specificity.

2. & behaves like :is()

Native nesting treats the parent selector as if it were wrapped in :is(). That has two consequences.

Specificity follows :is() rules. With a selector list parent, the nested selector takes the specificity of the most specific item in the list:

#main,
.content {
  p {
    color: #334155;
  }
}

Native CSS treats this like :is(#main, .content) p, which has ID-level specificity for both branches, including .content p. Sass would output #main p, .content p, where .content p has only class-level specificity. This can cause subtle override bugs when you migrate.

Selector matching can differ. Consider:

.a .b {
  .c & {
    color: red;
  }
}

Sass produces .c .a .b. Native CSS produces .c :is(.a .b), which also matches when .c sits between .a and .b in the DOM. In most real-world code the difference never shows up, but it's worth knowing when debugging.

3. Declarations after nested rules

In the original spec, declarations that came after nested rules were hoisted to the top, which could change the cascade order. The spec was updated so that declarations after a nested rule stay in their place (they're wrapped in a nested declarations rule internally), and current browsers follow the updated behavior. The safest habit is still to write all declarations first, then nested rules, which is what most style guides recommend anyway.

.panel {
  padding: 1rem;
  color: #0f172a;

  .panel-body {
    padding: 0;
  }
}

4. No compile step, no output to inspect

Sass nesting is flattened at build time, so DevTools shows you the compiled selectors. Native nesting is preserved in the browser, and DevTools shows the nested structure. Modern DevTools handle this well, but the experience is slightly different.

What Sass Still Offers

Nesting is covered. Here's how the rest of Sass's feature set compares with native CSS today.

Variables

Native custom properties are more powerful than Sass variables in most respects: they cascade, can be scoped to elements, and can change at runtime. Where Sass variables still help is in places custom properties can't go, most notably media query conditions:

// Sass: works
$bp-md: 768px;
@media (min-width: $bp-md) {
}
/* Native: does NOT work */
@media (min-width: var(--bp-md)) {
}

Custom media queries (@custom-media) are specified to fill this gap, but browser support is very limited, so treat it as a future feature and use PostCSS if you want it today.

Math and color functions

Native calc(), min(), max(), clamp(), round(), mod(), trigonometric functions, color-mix(), and relative color syntax now cover most of what Sass math and color functions did:

.button {
  --accent: #3b82f6;
  background: var(--accent);

  &:hover {
    background: color-mix(in oklch, var(--accent), black 12%);
  }
}

One advantage native CSS has here: these functions work with runtime values, so a theme switch updates the derived colors automatically. Sass's darken() only runs once, at build time.

Mixins and functions

This is the biggest remaining gap. Sass lets you define reusable blocks of declarations with parameters:

@mixin focus-ring($color: #38bdf8) {
  outline: 2px solid $color;
  outline-offset: 2px;
}

.button:focus-visible {
  @include focus-ring;
}

Native CSS mixins and custom functions (@mixin and @function) are being specified by the CSS Working Group, and some experimental implementations exist in Chromium, but they're not ready for cross-browser production use. In the meantime, you can approximate some mixin patterns with custom properties and shared utility classes, but it's not the same.

Loops, conditionals, and maps

Sass's @each, @for, @if, and maps are great for generating repetitive CSS, like a spacing utility scale:

$spaces: (
  1: 0.25rem,
  2: 0.5rem,
  4: 1rem,
  8: 2rem,
);

@each $key, $value in $spaces {
  .mt-#{$key} {
    margin-top: $value;
  }
}

CSS has nothing equivalent for generating rules. The newer if() function adds inline conditional values in Chromium-based browsers, but it's not a code-generation tool and isn't available everywhere yet. If you generate a lot of CSS programmatically, Sass or a PostCSS plugin still earns its keep.

Partials and modules

Sass's @use and @forward let you split code into partials and bundle them into one file. Native @import works but triggers separate network requests unless a bundler inlines them. Most build tools (Vite, Next.js, Parcel, Lightning CSS) handle CSS imports without Sass, so this is less of a reason to use Sass than it used to be.

Migrating from Sass to Native Nesting

If you decide to drop Sass, here's a sensible path:

  1. Audit for & concatenation. Search for &-, &_, and &__. Each one needs rewriting with full class names.
  2. Replace Sass variables with custom properties where possible. Keep a small set of build-time constants (breakpoints) if you use a tool like PostCSS.
  3. Replace color functions. darken($c, 10%) becomes color-mix(in oklch, var(--c), black 10%) or a relative color expression.
  4. Check selector list parents. Look for nested rules under comma-separated selectors that include IDs, and verify specificity hasn't changed.
  5. Rename .scss to .css and test. If you use a bundler, it will pass the nested CSS through untouched, or you can use Lightning CSS or PostCSS to flatten nesting for older browsers.

Here's a typical before and after:

// Before: Sass
$radius: 8px;

.alert {
  border-radius: $radius;
  padding: 1rem;

  &__icon {
    width: 20px;
  }

  &--error {
    background: lighten(#dc2626, 45%);
    color: #dc2626;
  }
}
/* After: native CSS */
:root {
  --radius: 8px;
}

.alert {
  border-radius: var(--radius);
  padding: 1rem;

  .alert__icon {
    width: 20px;
  }

  &.alert--error {
    background: color-mix(in srgb, #dc2626 12%, white);
    color: #dc2626;
  }
}

Best Practices for Native Nesting

  • Don't nest too deeply. The same advice from the Sass era applies: more than three levels usually means over-specific selectors that are hard to override.
  • Put declarations before nested rules. It keeps the cascade intuitive and reads more clearly.
  • Use nesting for states and responsive rules, where it adds the most readability: &:hover, &:focus-visible, @media, @container.
  • Combine with @layer to keep specificity manageable across your codebase.
  • Watch specificity with selector lists since & behaves like :is().

Browser Support

Native CSS nesting is supported in current versions of Chrome, Edge, Firefox, and Safari. The relaxed syntax (allowing nested selectors to start with an element name) is supported in all of them too. If you need to support older browsers, tools like Lightning CSS and postcss-nesting can compile nested CSS into flat CSS at build time. You keep writing standard syntax and simply drop the transform once your audience catches up.

Conclusion

For many projects, native nesting removes the main reason for using Sass. Combined with custom properties, color-mix(), relative colors, cascade layers, and bundler-level imports, plain CSS now covers most of what teams used Sass for.

Sass still makes sense if you rely on mixins with parameters, generate large amounts of CSS with loops and maps, or depend heavily on BEM-style & concatenation. But if your Sass files are mostly nesting and a few variables, try renaming one to .css and cleaning up the handful of differences. You may find you don't need the build step at all.

Tags :
Share :

Related Posts

A Complete Guide to CSS Container Queries

A Complete Guide to CSS Container Queries

For more than a decade, responsive design meant one thing: media queries. You asked the browser how wide the viewport was and adjusted your layout ac

Continue Reading
A Comprehensive Guide to Installing Next.js

A Comprehensive Guide to Installing Next.js

Next.js has emerged as a powerful framework for building React applications, offering features like server-side rendering, static site generation, an

Continue Reading
Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

CSS has evolved significantly, and modern tools like clamp(), min(), and max() are powerful game-changers in dynamic styling. If you’ve struggl

Continue Reading