Command Palette

Search for a command to run...

Hectal
PHASE 11Advanced Baseline 2023+ ~28 min· topic 1 of 8

Topic 11.1

CSS Nesting

In one line

Native CSS nesting lets you write a rule inside another rule, so styles for a component and its parts live together; & stands for the parent selector, and every nested selector behaves as if the parent were wrapped in :is(), which decides its specificity.

Think of it like this

A family address book. Instead of writing "Sharma family, Ravi", "Sharma family, Priya", "Sharma family, Dadi" on three lines, you write the heading Sharma family once and list the names indented under it. Everyone reading knows each name belongs to that family. Nesting does the same for CSS: write .card once, then put the rules for its title and its links inside it.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Nesting
Writing one CSS rule inside the braces of another, so the inner one only applies inside what the outer one matched.
Parent rule
The outer rule that contains nested rules, like .card { ... }.
Nested rule
A rule written inside another rule. Its selector is read relative to the parent's selector.
`&` (nesting selector)
Stands for the elements the parent selector matched. &:hover inside .card means .card:hover.
Descendant
An element anywhere inside another element: a child, a grandchild and so on.
At-rule
A CSS statement starting with @, like @media or @layer.
Specificity
The score the cascade uses to decide which selector is stronger (Phase 2).
`:is()`
A pseudo-class that matches any selector in its list. Its specificity is that of the most specific one in the list.
Preprocessor
A tool, like Sass, that turns an extended CSS language into plain CSS before the browser sees it.

Step by step

01The flat version

Here is a card written the old way. The selector .card appears four times. Rename the class and you must edit four places, and nothing in the file shows these rules belong together.

style.csswhole filecss
.card { padding: 16px; border: 1px solid #ddd; }
.card .title { margin-top: 0; }
.card a { color: #db2777; }
.card a:hover { text-decoration: none; }

02Nest the parts inside the component

Move the part rules inside .card. A nested selector with no & gets & in front automatically, so .title means .card .title and a means .card a. The result is identical; only the writing is shorter.

style.csswhole filecss
.card {
  padding: 16px;
  border: 1px solid #ddd;

  .title { margin-top: 0; }
  a {
    color: #db2777;
    &:hover { text-decoration: none; }
  }
}

03Use & to attach to the parent itself

States and variants of the component need & with no space: &:hover, &.featured, &[aria-current]. With a space (or with no &), the selector looks for a descendant instead.

style.csswhole filecss
.card {
  &.featured { background: #eef2ff; }   /* .card.featured */
  &:focus-within { outline: 2px solid; } /* .card:focus-within */
  & + & { margin-top: 12px; }          /* .card + .card */
  .dark & { background: #1f2937; }    /* .dark .card */
}

04Nest media queries and other at-rules

Put @media right inside the rule. Declarations inside it apply to .card. The browser turns it into @media (min-width: 600px) { .card { padding: 24px; } }.

style.csswhole filecss
.card {
  padding: 16px;
  @media (min-width: 600px) {
    padding: 24px;
  }
}

05Work out the specificity

Replace & with :is(parent) and score it. .card { & .title {} } is :is(.card) .title: (0,2,0). #hero, p { & span {} } is :is(#hero, p) span: (1,0,1), even for a span inside a p, because :is() uses its strongest argument.

Work out the specificitydiagram
Rendering diagram…

06Keep it shallow

Stop at two or three levels. Deep nesting builds long, strong selectors like .page .main .list .item a, which are slow to read and hard to override. When something needs styling deep down, give it a class.

Try it yourself

  1. 1

    Remove the &

    In the first example, change &.featured to .featured (no &). Predict: will the card still get the pale background? Run it. (It won't: .featured now means a descendant with that class.)

  2. 2

    Flip the parent position

    In the second example, change .dark & to & .dark. Predict what it means now (a .dark element inside a button) and which buttons change. Then check the console.

  3. 3

    Defuse the specificity surprise

    In the third example, change #hero, p to .hero, p. Predict the new specificity of the nested rule and the colour of the span. Then try writing the nested rule as p { & span {...} } alone.

Code & diagrams

A component written with nesting Baseline 2023+ New tab

The nested `p` rule only reaches paragraphs inside the card, so `.outside` keeps the default black. Adding the class `featured` turns on `&.featured`, which means `.card.featured`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

.card .title: rgb(67, 56, 202)
.card p: rgb(55, 65, 81)
.outside: rgb(0, 0, 0)
featured background: rgb(238, 242, 255)
& in different places, and nested @media New tab

`.dark &` puts the parent at the end: a button inside `.dark`. The plain button's previous sibling is the `<section>`, not a `.btn`, so `& + &` doesn't match it. Resize the window past 500px to see the padding change.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

inside .dark: rgb(245, 158, 11)
plain, margin-left: 0px
after a button, margin-left: 12px
The specificity surprise: & acts like :is() New tab

The span is inside a `p`, not inside `#hero`, yet the nested rule scores (1,0,1) because `:is()` takes its strongest argument. Crimson wins over the later `.text span`. Change the parent to `.hero, p` and seagreen wins.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

span colour: rgb(220, 20, 60)
How the browser reads nested rulesdiagram
Rendering diagram…

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget & before a pseudo-class

In the first example, change &:hover { text-decoration: none; } to :hover { text-decoration: none; }.

terminal
$ Hover the link
── what you'll see ──
The underline never disappears when you hover the link. DevTools shows the rule as `.card a :hover`, which matches nothing because the link has no hovered element inside it.

Break #2

Use Sass-style & concatenation

Write .card { &__title { color: red; } } expecting it to style .card__title.

terminal
$ Look at the title
── what you'll see ──
The title keeps its old colour. DevTools shows no rule for it: the nested rule was thrown away as invalid.

Myth vs fact

Myth

Native nesting is the same as Sass nesting.

Fact

Mostly, but not exactly. Native & is a real selector (no &__title string building), and its specificity follows :is() rules, while Sass pastes text, so #a, p { span {} } becomes two separate selectors with different specificities.

Myth

Nesting makes CSS faster.

Fact

Nesting changes how you write CSS, not how fast it matches. The browser expands nested rules into normal selectors. Deep nesting can even produce longer selectors that do more matching work.

Myth

You always need & in nested rules.

Fact

Only when the selector must attach to the parent itself (&:hover, &.active) or put the parent somewhere else (.dark &). A plain descendant like .title { } works without it.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Nesting reached Chrome 112, Safari 16.5 and Firefox 117 in 2023. The first version required nested selectors to start with a symbol (so & p instead of p); the "relaxed" syntax that allows p { } arrived in Chrome 120 and Safari 17.2. If you must support older browsers, a PostCSS plugin or Lightning CSS can flatten nesting at build time.

  • ▸

    A 2024 spec change (Chrome 130 and later browsers) wraps declarations that come after a nested rule into a hidden "nested declarations" rule, so they keep their written order. Before that, such declarations were hoisted above the nested rules. Avoid relying on either: put a rule's own declarations first, then its nested rules.

  • ▸

    Because the parent is wrapped in :is(), nesting can't express the Sass @extend or string tricks, but it handles pseudo-elements carefully: :is() can't contain pseudo-elements, so .a::before { & .b {} } doesn't match anything. Nest under elements, not under ::before or ::after.

  • ▸

    In the CSSOM (CSS Object Model, the JavaScript view of stylesheets), a nested rule appears in its parent's cssRules list, and its selectorText is what you wrote (& .title), not the expanded selector.

Remember this

  1. 1

    Without nesting, every rule about a component repeats its selector: .card { }, .card .title { }, .card a { }, .card a:hover { }. Native CSS nesting lets you write a nested rule (a rule inside another rule's braces) instead: .card { padding: 16px; .title { color: navy; } }. The browser reads the inner rule as .card .title. The outer rule is called the parent rule. This used to need a preprocessor like Sass (Topic 11.5); since 2023 every major browser understands it directly.

  2. 2

    The nesting selector & means "whatever the parent selector matched". You need it whenever the nested selector must attach to the parent itself instead of to a descendant (an element somewhere inside it). &:hover means "the card itself when hovered" (.card:hover), &.featured means "a card that also has the class featured" (.card.featured), and & + & means "a card right after another card". Without the &, a nested :hover { } means .card :hover: any element inside the card that is hovered, which is a classic bug.

  3. 3

    & can go anywhere in the nested selector, not only at the start. .dark & { } inside .btn means .dark .btn: a button inside a dark section. When a nested selector has no & at all, the browser adds & (with a space) in front of it, so .title { } means & .title, a descendant. Since late 2023 browsers also accept nested selectors that start with an element name, like p { } (the first version of the feature required & p).

  4. 4

    Nested selectors are not glued together as text. Sass lets you write &__title to build the class name .card__title; native CSS does not, because the browser treats & as a real selector, not as text to paste. &__title is an invalid selector there, so the whole nested rule is dropped. Write the full class name instead: .card__title { } or & .card__title { }.

  5. 5

    Specificity of nested rules follows one rule: the parent selector is treated as if it were wrapped in :is(). .card { .title { } } is :is(.card) .title, specificity (0,2,0), the same as writing .card .title. The surprise comes with selector lists: #hero, p { & span { } } is :is(#hero, p) span, and :is() takes the specificity of its most specific argument, so even a p span match counts as (1,0,1), with the id's weight. (Phase 2 explains the (a,b,c) score; Phase 1 explains :is().)

  6. 6

    You can also nest at-rules (rules that start with @) such as @media, @supports, @container and @layer inside a style rule. Declarations written directly inside them apply to the parent selector: .card { padding: 8px; @media (min-width: 600px) { padding: 24px; } }. This keeps a component's responsive behaviour next to its base styles instead of in a separate block at the bottom of the file.

  7. 7

    Good nesting is shallow. Each level adds to specificity and makes selectors longer and more tied to the HTML structure. A useful limit is two or three levels: the component, its parts, and states like &:hover. If you find yourself nesting .page .main .list .item a span, the CSS will be hard to override later; give the element its own class instead.

Explain it without notes

01

What does & mean in native CSS nesting, and when must you write it?

02

How is the specificity of a nested selector calculated? Give an example where it surprises people.

03

What are the differences between native CSS nesting and Sass nesting?

04

Why do style guides limit nesting depth?

Practice

01

Rewrite this with nesting: .nav { display: flex; } .nav a { color: white; } .nav a:hover { color: gold; } .nav a.active { font-weight: bold; }

02

Inside a .btn rule, write nested CSS that gives the button a red background when it is inside an element with the class danger-zone, and a 20px top margin when it directly follows another .btn.

03

A stylesheet has .list { li { color: navy; } } and later ul li { color: teal; }. What colour is an li inside <ul class="list">, and why?

Trade-offs

  • ↔

    Nesting groups a component's rules and removes repetition, but it hides the real selector you'd see in DevTools and makes it easy to build long, high-specificity selectors without noticing.

  • ↔

    Native nesting needs no build step, but if you still support browsers from before 2023 you need a build tool to flatten it, which removes much of the benefit over Sass.

Done when you can

  • I can write nested rules for a component's parts, states and variants.

  • I know when & is required and what a nested selector means without it.

  • I can place & at the end (.dark &) and between selectors (& + &).

  • I can calculate the specificity of a nested selector using the :is() rule.

  • I can nest @media inside a rule.

  • I know why &__title works in Sass but not in native CSS.