← Back to Index

Lesson 12

CSS Nesting

If you've used Sass or Less, you already know how nesting works conceptually - writing child selectors inside their parent to keep related styles together. As of 2023, native CSS supports nesting directly in the browser. No build step, no preprocessor, just the language itself.

This lesson covers the syntax, the & selector, nested at-rules, specificity behaviour, and how native nesting differs from Sass. By the end you'll be able to use nesting confidently in production CSS.

The mental model

Nesting is purely a convenience for authoring. It lets you co-locate related rules so you can see the relationship between parent and child styles at a glance. But it doesn't change how the cascade resolves conflicts - the browser de-sugars nested rules into flat equivalents before applying them.

/* Nested */
.card {
  padding: 1rem;

  & .title {
    font-weight: 600;
  }
}

/* De-sugars to exactly this: */
.card { padding: 1rem; }
.card .title { font-weight: 600; }

Nesting is syntactic sugar. The browser converts nested rules to flat selectors before resolving styles. Specificity, cascade order, and inheritance all work identically to the flat equivalents. Nesting just makes the code easier to read and maintain.

Basic nesting with &

The & character represents the parent selector - whatever selector wraps the current nesting level. You use it to compose the final selector that the browser will apply:

.card {
  background: white;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 1rem;

  /* & .title  →  .card .title (descendant) */
  & .title {
    font-weight: 600;
    color: var(--accent);
  }

  /* & .body  →  .card .body */
  & .body {
    font-size: 0.85rem;
    color: #555;
  }

  /* &:hover  →  .card:hover (pseudo-class) */
  &:hover {
    border-color: var(--accent);
    box-shadow: 0 2px 8px rgba(58, 134, 255, 0.1);
  }
}

Hover to see the nested &:hover rule in action

Card Title
This card uses native CSS nesting. Hover over it to see the border and shadow change - that's the &:hover rule firing.

When & is required vs optional

The & is sometimes optional and sometimes required. Here's when you need it:

.btn {
  /* These two are equivalent for descendant selectors: */
  & .icon { margin-right: 0.5rem; }
  .icon { margin-right: 0.5rem; }

  /* & is REQUIRED for pseudo-classes (starts with :) */
  &:hover { background: darkblue; }
  /* Without &, the browser can't parse it correctly */

  /* & is REQUIRED for compound selectors (no space) */
  &.primary { background: blue; }
  /* This means .btn.primary, NOT .btn .primary */

  /* & AFTER another selector - reverses the relationship */
  .dark-mode & { background: #333; }
  /* Produces: .dark-mode .btn { background: #333; } */
}

Rule of thumb: If the nested selector should attach directly to the parent (no space between them), you need &. If it's a descendant (space between parent and child), & is optional but improves clarity.

Compound selectors: & with no space

One of the most common uses of & is creating compound selectors - selectors that target the same element with multiple conditions.

Quick refresher: space vs no space in selectors

Selector Meaning Nesting syntax
.card .featured Element with class featured inside an element with class card & .featured { }
.card.featured Element with both classes card and featured on the same element &.featured { }
.card {
  border: 1px solid var(--border);

  /* Compound: same element has both .card AND .featured */
  &.featured {
    border-color: #e76f51;
    border-width: 2px;

    & .title {
      color: #e76f51;
    }
  }

  /* Descendant: .featured is a CHILD element inside .card */
  & .featured {
    font-style: italic;
  }
}

Compound selector demo - the card below has both .nest-card and .featured classes

Nesting media queries

One of the most practical benefits of nesting is co-locating responsive styles with the component they affect. Instead of scrolling to a separate @media block at the bottom of your file, you nest the query right inside the selector:

.grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;

  @media (min-width: 500px) {
    grid-template-columns: 1fr 1fr;
  }

  @media (min-width: 900px) {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

/* De-sugars to: */
.grid { display: grid; grid-template-columns: 1fr; gap: 1rem; }
@media (min-width: 500px) { .grid { grid-template-columns: 1fr 1fr; } }
@media (min-width: 900px) { .grid { grid-template-columns: 1fr 1fr 1fr; } }

The nested @media is unwrapped by the browser into a normal media query block wrapping that selector. It's purely co-location for readability - the effect is identical to writing the media query separately.

Resize the browser below 500px to see the grid switch from two columns to one

Item 1
Item 2
Item 3
Item 4

Co-location is the benefit. Nested media queries don't change behaviour - they de-sugar to the same flat @media blocks. The advantage is purely organisational: all of a component's responsive logic lives together instead of scattered across separate breakpoint sections.

Ad

Nesting other at-rules

Media queries aren't the only at-rules you can nest. Container queries (@container) and feature queries (@supports) work the same way:

.sidebar {
  width: 100%;

  /* Nested container query */
  @container (min-width: 300px) {
    display: grid;
    grid-template-columns: 1fr 1fr;
  }
}

.fancy-text {
  font-size: 1rem;

  /* Nested feature query */
  @supports (font-size: clamp(1rem, 2vw, 2rem)) {
    font-size: clamp(1rem, 2vw, 2rem);
  }
}

/* Both de-sugar to normal at-rule blocks wrapping the selector,
   just like nested @media queries. */

Any conditional at-rule can be nested inside a selector. The browser unwraps it into the equivalent flat structure.

Specificity of nested selectors

Nested selectors have exactly the same specificity as their flat equivalents. Nesting does not increase or decrease specificity in any way:

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

/* Flat equivalent - IDENTICAL specificity (0, 2, 0) */
.card .title { color: blue; }

/* Both have specificity (0, 2, 0) - two class selectors */

However, deep nesting can accidentally create high-specificity selectors without you realising it:

/* Looks innocent... */
.page {
  & .section {
    & .card {
      & .title {
        & span {
          color: red;
        }
      }
    }
  }
}

/* But de-sugars to: */
.page .section .card .title span { color: red; }
/* Specificity: (0, 4, 1) - very hard to override! */

Keep nesting shallow - 2 to 3 levels maximum. Each nesting level adds to the final selector's specificity. Deep nesting creates the same specificity problems that made old-school CSS hard to maintain. Just because you can nest deeply doesn't mean you should.

CSS nesting vs Sass nesting - differences

If you're coming from Sass, native CSS nesting is very similar but has some important differences:

Feature Sass Native CSS
& for parent reference Yes Yes
String concatenation with & &__element produces .block__element Not supported - & is a selector, not a string
Nesting at-rules Yes Yes
Descendant without & .child { } implies descendant .child { } also implies descendant (relaxed syntax)
Variables $variables, @mixin, @include Custom properties only - no mixins
Build step required Yes - compiles to CSS No - runs directly in the browser

The biggest difference is that native CSS & cannot do string concatenation. In Sass, &__title inside .card produces .card__title. In native CSS, this doesn't work - & represents the full selector object, not a string you can append to. BEM-style concatenation still requires Sass or manual flat selectors.

/* Sass - works */
.card {
  &__title { font-weight: bold; } /* → .card__title */
  &--large { font-size: 2rem; }   /* → .card--large */
}

/* Native CSS - does NOT work */
.card {
  &__title { ... } /* Invalid! & is not a string */
}

/* Native CSS - you'd write these flat instead: */
.card__title { font-weight: bold; }
.card--large { font-size: 2rem; }

Browser support

Native CSS nesting is supported in Chrome 120+, Firefox 117+, Safari 17.2+, and Edge 120+. As of mid-2024, this covers approximately 85% of global users. For older browsers, nested rules are simply ignored (they won't break anything, but the styles won't apply). Consider your audience before relying on nesting in production without a build fallback.

/* Progressive enhancement approach */
/* Flat fallback for older browsers */
.card .title { font-weight: 600; color: var(--accent); }

/* Nested version for modern browsers - won't conflict
   since specificity is identical */
.card {
  & .title { font-weight: 600; color: var(--accent); }
}

/* Or use @supports if you need conditional logic */
@supports selector(&) {
  /* Browser supports nesting */
}

Retrieval check

Question 1

What does & represent in CSS nesting?

The parent selector (the outer rule's selector)
The nearest HTML parent element
A placeholder for any class name
The document root selector

Question 2

What is the difference between &.featured and & .featured?

They are identical - the space doesn't matter
&.featured = same element (compound). & .featured = descendant (with space)
&.featured targets children, & .featured targets siblings
&.featured is invalid syntax

Question 3

Does nesting change specificity compared to the flat equivalent?

Yes - nesting increases specificity by one level per depth
Yes - nested selectors get a specificity bonus
No - specificity is identical to the equivalent flat selector
Only if you use & explicitly

Question 4

What does nesting a @media rule inside a selector do?

Creates a scoped media query that only applies to that selector's children
De-sugars to a normal @media block wrapping that selector - just co-locates the code
Increases the media query's priority in the cascade
Makes the media query inherit the parent's specificity

Question 5

What is a good maximum nesting depth to maintain readable CSS?

1 level - never nest deeper than one level
2-3 levels - keeps specificity manageable
5-6 levels - matches typical DOM depth
No limit - nesting depth doesn't affect specificity