CODE CRISPIES

Native CSS Nesting — Stop Compiling for Sass-Style Code

3 min read cssarchitecture

The single biggest reason teams kept Sass after CSS variables landed was nesting. That excuse is gone — browsers ship native CSS Nesting in plain .css files. No build step, no @import, no preprocessor.

The basic syntax

.card {
  background: white;
  padding: 1rem;
  border-radius: 8px;

  &:hover {
    background: #f3f4f6;
  }

  & h2 {
    margin-top: 0;
    font-size: 1.25rem;
  }

  & .meta {
    color: #6b7280;
  }
}
css

Reads identical to Sass. Compiles down to:

.card { background: white; padding: 1rem; border-radius: 8px; }
.card:hover { background: #f3f4f6; }
.card h2 { margin-top: 0; font-size: 1.25rem; }
.card .meta { color: #6b7280; }
css

But there is no compile step. The browser does it.

The & is now mandatory (in modern syntax)

Original CSS Nesting required & always. Modern browsers (Chrome 120+, Safari 17.2+, Firefox 117+) accept it implicitly:

/* Modern: works without & */
.card {
  h2 { font-size: 1.25rem; }    /* implies & h2 */
  .meta { color: #6b7280; }     /* implies & .meta */
}
css

Whether to use explicit & is style preference — Sass habit says yes, brevity says no. Both compile identically.

Where it shines: media queries

.card {
  padding: 1rem;

  @media (min-width: 600px) {
    padding: 1.5rem;
  }

  @media (min-width: 900px) {
    padding: 2rem;
    display: grid;
    grid-template-columns: 200px 1fr;
  }
}
css

Component + its breakpoints in one block. No more "find the matching @media 200 lines below."

Container queries nest the same way

.card-container {
  container-type: inline-size;
}

.card {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;

  @container (min-width: 400px) {
    flex-direction: row;
    gap: 1rem;
  }
}
css

Pseudo-class composition

.btn {
  background: var(--brand);

  &:hover { background: var(--brand-light); }
  &:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
  }
  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }

  &.btn-primary { color: white; font-weight: 600; }
  &.btn-ghost { background: transparent; border: 1px solid currentColor; }
}
css

Variants and states colocated with the base rule.

The one Sass thing CSS still can't do

Sass's @mixin / @include and @function have no native CSS equivalent yet. If your codebase relies heavily on parameterized mixins, Sass remains useful. For everything else (variables, nesting, color manipulation via color-mix, :is() / :where()), plain CSS is enough.

For functions specifically, watch CSS Functions Module Level 1@function is in the spec, just not shipped yet.

What changes in practice

I converted a 1500-line Sass file to plain CSS last month:

Browser support

Caniuse:

Everything from 2024+. For older browsers, the entire nested rule fails to parse — so this is not progressive enhancement. Either gate via @supports selector(&) or accept that pre-2024 browsers see no styles for those rules.

@supports selector(&) {
  .card {
    & h2 { font-size: 1.25rem; }
  }
}
css

For most modern audiences, the @supports guard isn't worth the verbosity.


Hands-on selector practice in css-basic-selectors on Code Crispies — interactive lessons with live preview.