CSS Nesting

CSS nesting lets you write a rule inside another rule, so a component's parts, its hover state and its media queries live together in one place instead of scattered through the file.

Journeyman · CSS · Updated

After this lesson you can

  • Nest related rules inside their parent rule, using & for states such as :hover and for nested media queries
  • Work out the full selector and specificity a nested rule ends up with, and keep nesting three levels deep or less

Key takeaways

  • A rule nested inside another applies to elements inside the parent. .game-card { h3 { } } styles the h3 inside a game card.
  • & (ampersand) is the nesting selector: it stands for the parent. &:hover is the parent while hovered; without the &, :hover means a hovered element inside it.
  • A media query can sit inside a rule. Its declarations apply to that rule's elements when the condition is true.
  • A nested rule scores as if its parent were wrapped in :is(), so a parent list with an id makes every nested rule as strong as an id.
  • Three levels deep is plenty. Deeper nesting builds long, strong selectors that copy the HTML and are hard to override.

You're styling a game card for Meeple, a board game café: the card itself, its title, the line with the number of players, a "Staff pick" badge, and what happens when the mouse moves over it. In a flat stylesheet that's five rules, each starting with .game-card, and a year later a sixth turns up 200 lines away because someone added it at the bottom. With nesting, everything about the card lives inside the card's rule, the way everything about one recipe sits on one recipe card. Most stylesheets written today use it, and if you've seen Sass (.scss files) in a project or a tutorial, it's the same idea, now built into CSS.

Rules that repeat themselves

Here is Meeple's game card, and its styles written the flat way:

<li class="game-card">
  <h3>Ticket to Ride</h3>
  <p class="game-meta">2 to 5 players · 45 minutes</p>
  <span class="badge">Staff pick</span>
</li>
.game-card {
  border: 2px solid #5b2a86;
  padding: 1rem;
}

.game-card h3 {
  color: #3f1d5e;
}

.game-card .game-meta {
  color: #2a9d8f;
}

.game-card:hover {
  border-color: #2a9d8f;
}

Every rule repeats .game-card. Nothing keeps the four together: they could be anywhere in the file, and when the card is renamed, all four have to be found.

Rules inside rules

CSS nesting lets you write a rule inside another rule. A nested rule applies to elements inside the parent's elements, as if you'd written the parent's selector, a space, and the nested selector:

.game-card {
  border: 2px solid #5b2a86;
  padding: 1rem;

  h3 {
    color: #3f1d5e;
  }

  .game-meta {
    color: #2a9d8f;
  }
}

This means exactly what the first three flat rules meant: h3 inside the card becomes .game-card h3, and .game-meta becomes .game-card .game-meta. The card's own declarations come first, then the nested rules, indented one step, the same habit as indenting HTML inside its parent.

Try it. Make a page with Meeple's card from above, and write the nested rule in a <style> element. Inspect the <h3>: the Styles pane lists the nested rule among the rules that style it, with its color.

& means "the parent, right here"

The hover rule is different. .game-card:hover isn't something inside the card: it's the card itself, while the mouse is over it. For that, nesting has the nesting selector, &, read "ampersand". It stands for the parent's selector, right where you put it:

.game-card {
  border: 2px solid #5b2a86;

  &:hover {
    border-color: #2a9d8f;
  }

  .badge {
    background-color: #e9c46a;

    &.badge-new {
      background-color: #e76f51;
    }
  }
}
  • &:hover is .game-card:hover: the card, hovered.
  • &.badge-new, with no space, is .game-card .badge.badge-new: a badge that also has the class badge-new, such as <span class="badge badge-new">New</span>.

The space matters, as it does everywhere in CSS. & .badge-new, with a space, would mean "a .badge-new inside the badge". And :hover with no & at all means "any hovered element inside the card", which is the most common nesting mistake: the hover seems to do nothing, or it lights up the wrong part.

Nesting a media query

A media query applies rules only when something about the screen is true. @media (min-width: 48em), read "at media, min width 48 em", means "when the window is at least 48em wide", about 768 pixels: a tablet or wider. Module 4, Responsive Design, teaches them in full. Here, what matters is that one can sit inside a rule:

.game-card {
  padding: 1rem;

  @media (min-width: 48em) {
    padding: 1.5rem;
  }
}

Inside the media query, the declarations need no selector of their own: they apply to the parent, the card. So the card's small-screen padding and its wide-screen padding sit next to each other, instead of the wide-screen one living in a media query at the bottom of the file.

The selector a nested rule ends up with

Behind the scenes, the browser writes every nested selector out in full. It puts the parent in :is() (from Modern Selectors: :is(), :where() and :has()) and then follows the rule you wrote:

You write, inside .game-card The browser reads Specificity
h3 :is(.game-card) h3 (0, 1, 1)
&:hover :is(.game-card):hover (0, 2, 0)
&.badge-new, inside .badge :is(:is(.game-card) .badge).badge-new (0, 3, 0)

For a single parent, that's the same score as writing the selector out by hand: .game-card h3 also scores (0, 1, 1). The surprise comes when the parent is a list. :is() takes the score of the most specific selector in its list, and the whole list's score goes to every element it matches:

.game-card, #featured-game {
  h3 {
    color: #3f1d5e;
  }
}

This reads :is(.game-card, #featured-game) h3, which scores (1, 0, 1) for every card's heading, not only the featured one. A later .game-card h3 rule, at (0, 1, 1), can't change any of them. Keep ids out of the parent lists you nest in.

Three levels deep is plenty

Nesting can follow the HTML all the way down, and that's the trap:

main {
  .games {
    .game-list {
      .game-card {
        h3 {
          color: #3f1d5e;
        }
      }
    }
  }
}

That heading's selector is main .games .game-list .game-card h3, scoring (0, 3, 2). It's long, it's strong (a later rule needs at least as much specificity to change it), and it breaks the day someone moves the game list out of main. Two habits keep nesting useful:

  • Nest the things that belong to the parent: its parts, its states (&:hover, &:focus-visible) and its media queries.
  • Stop at three levels. If you're deeper than that, start a new top-level rule. Here, .game-card on its own is enough.

Try it. Write the five-level rule above for Meeple's card, then add a flat rule after it: .game-card h3 { color: #e76f51; }. Does the heading turn coral? Score both selectors to see why not. Then rewrite the deep rule as a .game-card rule with h3 nested inside, and try again.

Will it work in every browser?

Nesting works in every current major browser: Chrome and Edge since version 120, Firefox since 117 and Safari since 17.2, all from late 2023. It is Baseline widely available (since June 2026), the label web.dev gives a feature that has worked everywhere for more than two and a half years. A browser older than that skips nested rules it can't read, so a site that must work on very old devices writes them out flat.

Check yourself

2 questions to try on the lesson page.

So what

Nesting turns a stylesheet into a set of tidy boxes: each component's parts, states and screen sizes in one rule. Use & when you mean the parent itself, and stop at three levels, so the selectors stay short enough to override.

Key terms

Cheat sheets

Related