w3tweaks.com · CSS Tutorial

Native CSS Nesting

Sass-style nesting, zero build step — plus the trap Sass users never see coming

Tab 1

Nested vs Flat — Same Result, Half the Repetition

This nav component below is styled entirely with native nesting — inspect this page's CSS to verify. Hover and click the links.

Before — flat CSS (repetition)
.nav { padding: 10px; }
.nav ul { display: flex; }
.nav ul li { flex: 1; }
.nav ul li a { padding: 9px; }
.nav ul li a:hover { color: gold; }
.nav ul li a.active { background: gold; }
After — native nesting
.nav {
  padding: 10px;
  & ul {
    display: flex;
    & li a {
      padding: 9px;
      &:hover { color: gold; }
      &.active { background: gold; }
    }
  }
}

The & — Where It Goes Changes Everything

Four placements, four meanings. The chips below use &:hover and &.active — click them. Then toggle the context flip.

&:hover + &.active — click me
me too
I restyle when my ancestor is .dark — the "& at the end" trick
.chip {
  &:hover { }     /* .chip:hover — pseudo-class on self */
  &.active { }    /* .chip.active — compound, NO space */
  & .icon { }    /* .chip .icon — descendant, space */
  .dark & { }    /* .dark .chip — CONTEXT FLIP! */
  & + & { }      /* .chip + .chip — adjacent pairs */
}
Relaxed syntax: since late 2023 the & is optional for simple descendants — .card { h2 { } } works directly. You must use & for: compounds (&.active — without it, a space is inserted and the selector breaks), pseudo-classes (&:hover), and context flips (.dark &). Most 2026 style guides recommend always writing & for readability.
Tab 2

The :is() Specificity Trap — Where Sass Habits Break

Native nesting wraps the parent selector in :is() — and :is() takes the specificity of its most specific item. Nest inside a comma list containing an ID, and every branch gets ID-level specificity. This demo is live — the green override below genuinely loses.

Live proof — this class override SHOULD win, but doesn't:
I'm red — even though a later .override-attempt rule sets me green
/* Nested inside a comma list with an ID */
.trap-card, #trapFeatured {
  & .trap-h { color: red; }
}
/* Browser sees: :is(.trap-card, #trapFeatured) .trap-h
   → specificity (1,0,1) — ID strength for BOTH branches! */

/* This later override loses: (0,2,0) < (1,0,1) */
.override-attempt .trap-h { color: green; } ← defeated
Why Sass users get burned: Sass compiles .card, #featured { h2 { } } into two separate flat rules — .card h2 keeps class specificity. Native nesting produces :is(.card, #featured) h2 — one rule at ID strength. Identical source, different cascade. Fix: never mix IDs into nested comma lists, or split the ID into its own rule.

The Compound Whitespace Gotcha

Without &, the browser inserts a descendant space. .active nested bare means "descendant with .active" — not "self with .active".

&.active → styled ✓
bare .active → NOT styled (looks for a descendant)
.chip {
  .active { }  /* ❌ becomes ".chip .active" — descendant! */
  &.active { } /* ✅ becomes ".chip.active" — compound */
}

Invalid Nested Rule ≠ Dead Block

.parent {
  color: white;       /* ✓ works */
  & %invalid { }   /* ✗ only THIS rule ignored */
  & .valid { }     /* ✓ still works! */
}

Unlike a syntax error in a flat stylesheet (which can kill everything to the next brace), an invalid nested selector only drops its own rule — the parent and sibling rules survive. Forgiving by design.

Tab 3

Nesting At-Rules & the Sass Migration Map

Media queries, container queries, and feature queries can nest inside a selector — the component's responsive behavior lives with the component.

Live: a @container query nested INSIDE the card's rule — drag the corner:
Self-contained component
My container query lives inside my own CSS block:
.card {
  padding: 14px;

  @media (min-width: 768px) {
    padding: 24px/* no & needed — applies to .card */
  }

  @container (min-width: 340px) {
    & .status { color: green; }
  }

  @supports (display: subgrid) {
    display: subgrid;
  }
}

Can You Drop Sass? The Honest Map

Sass featureNative equivalentVerdict
Nesting (descendants, pseudo)& — native✓ Drop Sass
$variables--custom-properties✓ Drop Sass
Nested media queries@media inside rules✓ Drop Sass
Color functions (darken…)color-mix(), oklch from✓ Drop Sass
&__element (BEM concat)not supported✗ Keep Sass
@each loops / mapsno equivalent✗ Keep Sass
Mixins with logicpartially (vars + if())~ Depends
✗ The &__ Dealbreaker
/* Sass — builds .btn__icon */
.btn {
  &__icon { } ← works in Sass
}

/* Native CSS: & is a live object,
   not a string — no concatenation */
&__icon ← invalid, rule dropped

In Sass, & is a string you can concatenate. In native CSS it's a selector reference. BEM codebases built on &__element must either flatten those class names or keep Sass for those files.

📐 The 3-Level Rule
/* ❌ Mirroring the whole DOM */
.page { .main { .section { .card { .body { .title

/* ✅ Nest for STATES, not structure */
.card-title {
  &:hover { }
  &.featured { }
  @media (…) { }
}

Keep nesting ≤3 levels. Deep nesting produces long, over-specific selectors that are hard to override — the same disease Sass codebases died of. Nest pseudo-classes, modifiers, and at-rules; don't mirror your HTML tree.

🔄 Migration Path (from Sass)
1. grep for &__ and @each — those files stay Sass
2. $vars → --custom-properties
3. copy nesting blocks to .css — relaxed
   syntax means most work as-is
4. test in browser (no build!)
5. remove Sass build from migrated files

Sass and native nesting coexist fine — .scss for the two edge cases, .css for everything else, same project. Most teams find 80%+ of files migrate cleanly.

🛡️ Feature Detection
@supports selector(&) {
  /* nesting-dependent styles */
}

Nesting is Baseline (Chrome 120+, Firefox 117+, Safari 17.2+, 90%+ global) — fallbacks are rarely needed in 2026. For legacy targets, a PostCSS plugin flattens nesting at build time, giving you the syntax today with universal output.