Guide

CSS nesting explained

Harbor Commerce's product catalog stylesheet repeated .product-card on 47 descendant selectors — every hover, focus, and sale-badge variant duplicated the block prefix. Engineers wanted Sass-style colocation without reintroducing a preprocessor pipeline on a static site that already compiled Tailwind from src/input.css. Native CSS nesting (Baseline 2023, all evergreen browsers) let them nest modifiers inside .product-card { } blocks using the & parent selector, cut duplicated tokens by 41%, and keep specificity flat at 0,1,0 per modifier. CSS nesting is a language feature that lets you write descendant and compound selectors inside a parent rule block, mirroring DOM structure without repeating class prefixes. This guide covers implicit vs explicit nesting, & patterns, specificity and @layer interaction, Sass migration traps, the Harbor Commerce refactor, a technique decision table vs BEM and CSS Modules, pitfalls, and a production checklist alongside our CSS fundamentals guide.

Native nesting syntax

Before nesting, every descendant selector repeated its ancestors:

.product-card { padding: var(--space-4); }
.product-card:hover { box-shadow: var(--shadow-md); }
.product-card .badge { font-size: 0.75rem; }
.product-card .badge--sale { color: var(--color-sale); }

With nesting, child rules live inside the parent block:

.product-card {
  padding: var(--space-4);

  &:hover {
    box-shadow: var(--shadow-md);
  }

  .badge {
    font-size: 0.75rem;

    &--sale {
      color: var(--color-sale);
    }
  }
}

The browser desugars this to the flat selectors above. Nesting is not a preprocessor trick — it is part of the CSS cascade and participates in specificity exactly as if you had written the expanded form.

Implicit vs explicit nesting

A nested rule that starts with a combinator or type selector nests implicitly:

.card {
  h2 { font-size: 1.25rem; }   /* .card h2 */
  > img { border-radius: 8px; } /* .card > img */
}

A nested rule that starts with a class, ID, or pseudo-class must use & to attach to the parent; otherwise the parser treats it as an invalid or ambiguous relative selector:

.card {
  &.is-featured { border: 2px solid gold; }  /* .card.is-featured */
  &:focus-within { outline: 2px solid blue; } /* .card:focus-within */
}

The & token represents the full parent selector list. In .a, .b { &:hover { } } the expansion is .a:hover, .b:hover — one ampersand per parent compound.

Specificity, layers, and :has()

Nesting does not change specificity math: .card .badge is still 0,2,0; .card:hover is 0,2,0 (one class + one pseudo-class). Deep nesting is a common footgun — three levels of classes yields 0,3,0 and can beat token-level utilities unless you pair nesting with cascade layers:

@layer components {
  .product-card {
  padding: var(--space-4);

  &:hover { transform: translateY(-2px); }
  }
}

@layer utilities {
  .u-no-transform { transform: none; }
}

Layer order lets utilities beat nested component hover rules without !important. Nesting also pairs well with :has() for parent-conditional styling colocated with the component:

.checkout-row {
  &:has(input:user-invalid) {
    border-color: var(--color-error);
  }

  &:has(.coupon-applied) .total {
    color: var(--color-success);
  }
}

Keep :has() branches shallow; expensive selectors inside large lists can hurt style recalculation on low-end mobile.

Sass migration and tooling

Sass nesting is similar but not identical. Key differences when porting:

  • Parent reference required — Sass allowed .child { } inside .parent to mean .parent .child; native CSS requires & .child or implicit combinator starts.
  • No variables or mixins — use custom properties and @layer instead of @mixin.
  • &-suffix — BEM-style &__title still works: expands to .block__title when parent is .block.
  • PostCSS — older pipelines used postcss-nesting with slightly different rules; verify against native output before dropping the plugin.

Stylelint 16+ ships stylelint-config-standard rules for native nesting. Enable nesting-selector-no-missing-scoping-root to catch forgotten & on class-nested rules.

Harbor Commerce product card refactor (worked example)

Problem. The catalog shipped 312 lines across product-card.css and product-card-states.css. Sale, out-of-stock, and featured variants each re-declared .product-card prefixes; a rename to .catalog-card required 38 search-replace hits and missed two hover rules in QA.

Change. Merged into one nested file inside @layer components:

@layer components {
  .product-card {
    display: grid;
    gap: var(--space-3);
    border-radius: var(--radius-lg);
    background: var(--glass);

    &:hover { box-shadow: var(--shadow-md); }
    &:focus-within { outline: 2px solid var(--focus-ring); }

    &.is-featured { grid-column: span 2; }
    &.is-sold-out { opacity: 0.6; pointer-events: none; }

    .media { aspect-ratio: 4/3; overflow: hidden; }
    .title { font-weight: 600; }
    .price { font-variant-numeric: tabular-nums; }

    .badge {
      &--sale { background: var(--color-sale); }
      &--new { background: var(--color-accent); }
    }
  }
}

Results. Line count fell from 312 to 184 (−41%). Specificity audit: all component rules stayed at 0,1,0 or 0,2,0; no new !important. Rename test: changing the root class required one edit. Stylelint caught two missing & during CI on the first PR.

Lesson. Nesting pays off when one block owns many variants. For atomic utility-first pages, Tailwind classes in markup may still be simpler than nested component CSS.

Technique decision table

Approach Colocation When to choose
Native CSS nesting Variants inside component block Component CSS files, BEM blocks, no Sass pipeline, Baseline 2023+ audience
Flat BEM + single class Separate rules per selector Maximum grep-ability, legacy browsers, strict stylelint specificity caps
CSS cascade layers Layer stacks, not DOM tree Vendor sandboxes, utilities vs components override lanes
CSS Modules / scoped builds Per-file hashed classes React/Vue SPAs, collision-proof class names at scale
Utility-first (Tailwind) Classes in markup Rapid layout, design tokens in HTML, small component CSS surface

Nesting and layers compose: nest for readability inside a layer, use layers for conflict resolution across files. Neither replaces responsive breakpoints — nest media queries inside the component block for the same colocation benefit.

Common pitfalls

  • Specificity creep — four nested classes beat most utilities; cap depth at two levels or use layers.
  • Missing &.parent { .child { } } is invalid in native CSS; use & .child or start with a combinator.
  • Over-nesting type selectors.article { p { } } couples styles to markup; prefer classes for reusable components.
  • Assuming Sass parity@extend, parent selectors in mixins, and interpolation do not exist natively.
  • Bundle size myths — nesting does not shrink deployed CSS versus flat selectors (same expanded output); savings are developer ergonomics and fewer rename bugs.
  • Keyframe and @font-face — these at-rules cannot be nested inside style rules; keep them top-level.

Production checklist

  • Confirm Baseline 2023 browser support matches your analytics; provide flat CSS fallback only if you still serve IE11 (rare in 2026).
  • Place nested component files inside @layer components when using a layered design system.
  • Run Stylelint with native nesting rules; block PRs on missing & for class-first nested selectors.
  • Audit expanded selector specificity after nesting refactors; target 0,1,0–0,2,0 for component modifiers.
  • Limit nesting depth to two levels; extract subcomponents when blocks exceed ~80 lines.
  • Pair :has() nests with performance spot-checks on large DOM lists (product grids, comment threads).
  • Document &-suffix BEM convention in CONTRIBUTING if the team uses element/modifier suffixes.
  • Verify PostCSS nesting plugins are removed or set to "native" mode to avoid double-processing.
  • Include one visual regression screenshot per nested component state (hover, focus, disabled) in CI.
  • Re-evaluate utility-first vs nested CSS per surface — marketing pages may stay Tailwind-heavy while checkout widgets use nested files.

Key takeaways

  • Native CSS nesting colocates variants with their parent block using & and implicit combinator rules — no Sass required.
  • Expanded specificity matches flat selectors; deep nesting inflates scores unless layers or utilities contain it.
  • Class-first nested rules need &; Sass-trained teams miss this most often in migration.
  • Nesting improves maintainability and renames; it does not reduce bytes in the shipped stylesheet.
  • Combine nesting with @layer for component readability and predictable override lanes.

Related reading