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.
&:hoverinside.cardmeans.card:hover. - Descendant
- An element anywhere inside another element: a child, a grandchild and so on.
- At-rule
- A CSS statement starting with
@, like@mediaor@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.
.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.
.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.
.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; } }.
.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.
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
Remove the &
In the first example, change
&.featuredto.featured(no&). Predict: will the card still get the pale background? Run it. (It won't:.featurednow means a descendant with that class.) - 2
Flip the parent position
In the second example, change
.dark &to& .dark. Predict what it means now (a.darkelement inside a button) and which buttons change. Then check the console. - 3
Defuse the specificity surprise
In the third example, change
#hero, pto.hero, p. Predict the new specificity of the nested rule and the colour of the span. Then try writing the nested rule asp { & span {...} }alone.
Code & diagrams
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
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)`.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
Console
Nothing logged yet.Expected console output
inside .dark: rgb(245, 158, 11)
plain, margin-left: 0px
after a button, margin-left: 12pxThe 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
Console
Nothing logged yet.Expected console output
span colour: rgb(220, 20, 60)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; }.
Break #2
Use Sass-style & concatenation
Write .card { &__title { color: red; } } expecting it to style .card__title.
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
& pinstead ofp); the "relaxed" syntax that allowsp { }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@extendor 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::beforeor::after. - ▸
In the CSSOM (CSS Object Model, the JavaScript view of stylesheets), a nested rule appears in its parent's
cssRuleslist, and itsselectorTextis what you wrote (& .title), not the expanded selector.
Remember this
- 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
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).&:hovermeans "the card itself when hovered" (.card:hover),&.featuredmeans "a card that also has the classfeatured" (.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
&can go anywhere in the nested selector, not only at the start..dark & { }inside.btnmeans.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, likep { }(the first version of the feature required& p). - 4
Nested selectors are not glued together as text. Sass lets you write
&__titleto build the class name.card__title; native CSS does not, because the browser treats&as a real selector, not as text to paste.&__titleis an invalid selector there, so the whole nested rule is dropped. Write the full class name instead:.card__title { }or& .card__title { }. - 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 ap spanmatch counts as (1,0,1), with the id's weight. (Phase 2 explains the (a,b,c) score; Phase 1 explains:is().) - 6
You can also nest at-rules (rules that start with
@) such as@media,@supports,@containerand@layerinside 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
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
What does & mean in native CSS nesting, and when must you write it?
How is the specificity of a nested selector calculated? Give an example where it surprises people.
What are the differences between native CSS nesting and Sass nesting?
Why do style guides limit nesting depth?
Practice
Rewrite this with nesting: .nav { display: flex; } .nav a { color: white; } .nav a:hover { color: gold; } .nav a.active { font-weight: bold; }
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.
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
@mediainside a rule.I know why
&__titleworks in Sass but not in native CSS.