ESC

Type to search the knowledge base.

CSS Nesting

Native CSS nesting — & parent selector, nested rules and media queries, specificity behavior, and when not to nest five levels deep.

intermediate3 min read
  • css
  • nesting

Browsers now understand native CSS nesting — rules inside rules without a preprocessor. The & parent selector joins nested selectors to the outer one, matching the mental model many teams already have from Sass, with a few important differences.

Docs: MDN CSS nesting, CSS Nesting Module.

Basic form

.card {
  padding: 1rem;
  border: 1px solid var(--border);

  & .card__title {
    margin: 0;
    font-size: 1.125rem;
  }

  & .card__title {
    /* equivalent flattened: .card .card__title */
  }

  &:hover {
    border-color: var(--brand);
  }

  &.is-selected {
    box-shadow: 0 0 0 2px var(--brand);
  }
}
/* Also valid in modern nesting: start nested type selectors carefully */
.card {
  h2 {
    /* parses as & h2 → .card h2 in supporting engines with nesting */
    font-size: 1.125rem;
  }
}

Engine rules evolved: if a nested selector wouldn’t parse safely, prefix with &. Prefer explicit & for team clarity.

Nested media and supports

.toolbar {
  display: flex;
  gap: 0.5rem;

  @media (min-width: 48rem) {
    gap: 1rem;

    & .toolbar__label {
      display: inline;
    }
  }

  @supports (backdrop-filter: blur(4px)) {
    & {
      backdrop-filter: blur(8px);
    }
  }
}

Keeps responsive variants next to the component — the main readability win.

Specificity

Nested selectors compute like their flattened equivalents. Nesting is not a specificity shield.

.card {
  & .title {
    /* specificity of .card .title */
    color: blue;
  }
}

&:is(.a, .b) follows :is() specificity rules (most specific argument wins).

Composition patterns

.btn {
  --_bg: var(--brand);
  background: var(--_bg);
  color: white;

  &--primary {
    /* .btn--primary only if your naming expects concatenation */
  }
}

Warning: &--primary becomes .btn--primary (string concatenation with &). & .btn--primary is a descendant. Know which you want.

.btn {
  &.btn--primary {
    --_bg: var(--brand);
  }

  &.btn--danger {
    --_bg: var(--danger);
  }
}

How deep is too deep?

/* Smell */
.page {
  & .layout {
    & .sidebar {
      & .nav {
        & .link {
          & span {
            color: red;
          }
        }
      }
    }
  }
}

Flatten to BEM-style single classes or shallow nests (component + state + one media). Deep nests recreate the specificity and reuse problems preprocessors caused.

Interview out-loud

“Native nesting lets me write nested rules and media queries inside a component block using & for the parent. Flattened specificity matches the final selector list. I nest shallowly for hover, modifiers, and breakpoints, and I avoid deep descendant pyramids. It’s not Sass: some interpolation and parent tricks don’t exist.”

Footguns

  1. Expecting full Sass feature set (@mixin, @extend, maps).
  2. Accidental concatenation (&--x) vs descendant.
  3. Nesting until selectors are un-overridable.
  4. Shipping nested CSS to targets without support without a transpiler — check baseline.
  5. Mixing modules, layers, and deep nests without a stylelint max-depth rule.

Nesting + layers + modules

Native nesting works inside @layer blocks and CSS modules. With CSS modules, nested selectors still compile to unique class names for the local class; global tags inside nests can widen unexpectedly.

/* modules example */
.card {
  padding: 1rem;

  &:global(.is-dragging) {
    opacity: 0.8;
  }
}

Adopt a team max nesting depth (2–3) in stylelint. Nesting is a readability tool, not a replacement for good class names. If the flattened selector would look embarrassing in DevTools, rewrite it flat.

Migration from Sass

Replace @include with CSS mixins via custom properties or shared utility classes. Replace Sass maps with design tokens on :root. Keep Nesting; drop Sass-only selector juggling. Compile step can stay for older browsers until your baseline includes native nesting.

Further reading

Related guides