CSS Nesting

CSS · Lesson 33 of 55 6 min read Updated

When you style a component, you write the same class again and again: .card, .card h2, .card p, .card:hover. CSS nesting lets you write the child rules inside the parent rule, so everything about the card lives in one block.

For years this needed a preprocessor such as Sass. Today nesting is built into CSS and works in all current browsers. In this lesson you will learn how to nest rules, how the & selector works, how to nest media queries, and where native nesting differs from Sass.

Before and after

Here is a small card written the classic way and with nesting. Both produce exactly the same result.

CSS
/* Classic CSS */
.card { padding: 16px; border: 1px solid #ccc; }
.card h2 { margin: 0; }
.card p { color: #555; }
.card:hover { border-color: #2563eb; }

/* Nested CSS */
.card {
  padding: 16px;
  border: 1px solid #ccc;

  h2 { margin: 0; }
  p { color: #555; }

  &:hover { border-color: #2563eb; }
}

A nested rule like h2 { ... } inside .card means .card h2: an <h2> inside a card. The browser adds the parent selector and a space for you.

The & nesting selector

& stands for "the parent selector". You need it whenever the nested selector must attach to the parent itself instead of to something inside it.

You write inside .cardIt meansSelects
h2 { }.card h2An <h2> inside a card
&:hover { }.card:hoverThe card while hovered
&.featured { }.card.featuredA card that also has the class featured
& .featured { }.card .featuredAn element with class featured inside a card
> p { }.card > pA paragraph that is a direct child of a card
.dark &.dark .cardA card inside an element with class dark
& + & { }.card + .cardA card that directly follows another card

Look closely at &.featured and & .featured. The only difference is one space, but they select completely different elements. The last two rows show that & can go anywhere in the selector, not only at the start.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Nesting a card</title>
  <style>
    body { font-family: system-ui, sans-serif; margin: 16px; }

    .card {
      max-width: 340px;
      padding: 16px;
      border: 1px solid #cbd5e1;
      border-radius: 10px;

      h2 {
        margin: 0 0 6px;
        font-size: 1.2rem;
      }

      p {
        margin: 0;
        color: #475569;
      }

      &:hover {
        border-color: #2563eb;
      }

      &.featured {
        border-color: #f59e0b;
        background: #fffbeb;
      }

      & + & {
        margin-top: 12px;
      }
    }
  </style>
</head>
<body>
  <article class="card">
    <h2>Python for beginners</h2>
    <p>Evening batch, Rohtak.</p>
  </article>
  <article class="card featured">
    <h2>Web design weekend course</h2>
    <p>Saturday and Sunday, Jaipur.</p>
  </article>
</body>
</html>

Nesting media queries and other at-rules

You can also put @media, @container and @supports rules inside a style rule. Declarations written directly inside them apply to the parent selector. This keeps the responsive behaviour of a component next to the rest of its styles instead of in a separate block at the bottom of the file.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Nested media query</title>
  <style>
    body { margin: 0; font-family: system-ui, sans-serif; }

    .menu {
      display: grid;
      gap: 4px;
      padding: 8px;
      background: #1e293b;

      a {
        padding: 10px 12px;
        border-radius: 6px;
        color: white;
        text-decoration: none;

        &:hover,
        &:focus-visible {
          background: #334155;
        }
      }

      @media (width >= 36rem) {
        display: flex;
        gap: 8px;
      }
    }
  </style>
</head>
<body>
  <nav class="menu">
    <a href="#">Home</a>
    <a href="#">Courses</a>
    <a href="#">Results</a>
    <a href="#">Contact</a>
  </nav>
</body>
</html>

On a narrow screen the links stack in a grid. From 36rem they line up in a row. The @media block sits inside .menu, so display: flex applies to .menu. Also notice the two levels of nesting for the links: a inside .menu, then &:hover inside a, which means .menu a:hover.

How native nesting differs from Sass

If you have used Sass, most nesting will feel familiar, but there are two important differences.

No joining of names. In Sass, &__title inside .card creates .card__title. Native CSS does not join text like this. & stands for a whole selector, so &__title does not produce .card__title. If you use BEM class names (covered in Organising CSS), write the full class name as its own rule.

CSS
/* Works in Sass, does NOT work in native CSS */
.card {
  &__title { font-weight: bold; }
}

/* Native CSS: write the full class name */
.card__title { font-weight: bold; }

Specificity follows :is(). Native & behaves like wrapping the parent selector in :is(). If the parent is a list such as #sidebar, .panel, a nested rule takes the specificity of #sidebar, the most specific item, even when it matches through .panel. See :has(), :is() and :where() for why.

Keep nesting shallow

Nesting makes it easy to build long selectors without noticing. Three levels deep, .page .content .card h2 a has high specificity and depends on the exact HTML structure. Change the HTML and the style breaks; try to override it and you need an even longer selector.

A good rule: nest states (&:hover, &:focus-visible), variants (&.featured) and media queries freely, but nest child elements only one level deep. Also write a rule's own declarations first and its nested rules after them, so the block is easy to read from top to bottom.

Common mistakes

  • Forgetting & before a pseudo-class. Inside .btn, :hover { } means .btn :hover, which is any hovered element inside the button. Write &:hover.
  • Adding a space by accident. & .active and &.active select different things. The space means "inside".
  • Trying Sass-style name joining. &__title and &-large do not create new class names in native CSS.
  • Nesting too deep. Long nested chains create high specificity and fragile CSS. Keep it to one or two levels.

Key points

  • Native CSS nesting works in all current browsers; you no longer need a preprocessor for it.
  • A nested selector without & matches descendants: p { } inside .card means .card p.
  • & represents the parent selector; use it for states (&:hover), compound classes (&.featured) and context (.dark &).
  • @media, @container and @supports can be nested; their declarations apply to the parent selector.
  • Native & does not join text like Sass, and its specificity works like :is().
  • Keep nesting shallow to avoid high specificity.

Exercise

Rewrite this CSS using nesting. It should produce the same result.

CSS
.btn { padding: 8px 16px; background: #2563eb; color: white; }
.btn:hover { background: #1d4ed8; }
.btn:focus-visible { outline: 3px solid #f59e0b; }
.btn.danger { background: #dc2626; }
.btn svg { width: 16px; }
@media (width >= 48rem) { .btn { padding: 12px 24px; } }
Show solution
CSS
.btn {
  padding: 8px 16px;
  background: #2563eb;
  color: white;

  &:hover { background: #1d4ed8; }
  &:focus-visible { outline: 3px solid #f59e0b; }
  &.danger { background: #dc2626; }

  svg { width: 16px; }

  @media (width >= 48rem) {
    padding: 12px 24px;
  }
}

The states and the danger variant attach to the button with &. The svg rule needs no & because it selects an element inside the button. The media query's declarations apply to .btn.

Quick Quiz

1. Inside a .nav rule, what does a { color: white; } select?

2. Which nested selector matches a .tab element that also has the class active?

3. Inside .card, what does &__body do in native CSS?

4. Inside .btn, why is :hover { } a bug?

Learn CSS with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call