CSS nesting lets you write a rule inside the rule it modifies, so the selector context is stated once and the sub-rules hang underneath it. This page explains what the nesting selector really means, when you can leave it out, and exactly how nesting interacts with specificity — because nesting changes how you write CSS without changing how the cascade resolves it.
What problem does nesting actually solve?
Look at a card written the flat way and count how often the same class appears. The base rule needs it. The title rule needs it. The body rule needs it. The hover rule needs it. The active-state rule needs it, and so does the rule that styles the title inside the active card. Six rules, six copies of .card, and every one of them has to be found and edited if the class is ever renamed.
Nesting moves the shared context into one place. The base declarations stay in the parent rule, and every rule that depends on that context moves inside it. The file then reads in the order a person would explain it: here is what a card is, here is how it looks when hovered, here is what its title looks like, here is how it changes when the card is selected.
/* Flat: every rule repeats the parent selector */
.card {
border: 1px solid #d7e0e8;
border-radius: 12px;
}
.card .title {
font-size: 1.15rem;
margin: 0 0 0.4rem;
}
.card .body {
color: #43586d;
}
.card:hover {
border-color: #2f6fed;
}
/* Nested: the context is written once */
.card {
border: 1px solid #d7e0e8;
border-radius: 12px;
.title {
font-size: 1.15rem;
margin: 0 0 0.4rem;
}
.body {
color: #43586d;
}
&:hover {
border-color: #2f6fed;
}
}What does the nesting selector mean?
The ampersand is called the nesting selector, and it stands for the elements matched by the parent rule. Formally it desugars to an :is() wrapper around the parent's selector list, which is why it carries the same specificity rules as :is(). It can sit in several positions, and each one expresses a different relationship.
Written directly in front of a pseudo-class, &:hover, it targets the parent element itself in that state. Written as & .title, it targets a descendant. Written as > .title, it targets a direct child, because a nested selector that begins with a combinator is treated as a relative selector rather than a descendant one. Written as .theme-dark &, it flips the relationship and targets the parent when it sits inside a dark theme. Written as &.is-active, it adds a class to the parent element.
Two limits are worth knowing. The nesting selector cannot represent pseudo-elements: if your parent list includes .card::before, the & still only matches the .card element. And native CSS does not concatenate selector strings. In a preprocessor you may write &--wide and get .card--wide; in CSS that is not valid, because the ampersand is a separate token and the part after it would be read as a type selector, which is not allowed to follow the nesting selector. Use &.is-wide for a modifier class, or write .card--wide at the top level where it belongs.
.card {
/* same element, in a state */
&:hover {
border-color: #2f6fed;
}
/* same element with an extra class */
&.is-active {
box-shadow: 0 0 0 3px #2f6fed33;
}
/* a direct child */
> .title {
font-size: 1.15rem;
}
/* a descendant */
.meta {
color: #6b7c8d;
}
/* the parent, when it is inside another component */
.theme-dark & {
border-color: #33465a;
}
}When can you leave the ampersand out?
When the nested rule is a descendant relationship, you do not need the ampersand at all. A nested .title rule inside .card is equivalent to the flat .card .title rule, and the relaxed form is supported in current evergreen browsers. If you need to support older engines, write the ampersand explicitly or compile the stylesheet with a build tool.
When the nested rule begins with a combinator, the ampersand is implied but the combinator sets the relationship. The rule .card { > .title { ... } } is exactly .card > .title, and .card { & > .title { ... } } is the same rule with the same specificity. Either form works.
One style note from the specification itself: put the plain declarations of a rule before its nested rules. It reads better, and because a nested rule is treated as coming after its parent in the order of appearance, it keeps the order of the cascade predictable in your head as well as in the parser.
Does nesting change specificity?
No, and this is the part most people get wrong. Nesting is a way of writing CSS, not a way of changing how the cascade resolves it. A nested rule has exactly the specificity it would have had if you had flattened it by hand.
The ampersand contributes the specificity of the most specific complex selector in the parent's selector list, following the same rule as :is(). So .card { &.is-active { ... } } is (0,2,0), identical to the flat .card.is-active. And .card { .title { ... } } is (0,2,0), identical to the flat .card .title. Neither nesting nor the ampersand adds a step to the class column on its own.
Order of appearance is where nesting does change the outcome. A nested rule is considered to come after its parent rule, so in the first example below the nested declaration wins even though both have the same specificity. If you want the opposite, you can wrap the ampersand in :where(), which reduces the nesting selector's contribution to zero and lets the parent's declaration win.
/* Both declarations are (0,0,1).
The nested rule comes later in the order of appearance,
so red wins. */
article {
color: #1c4fa3;
& {
color: #b3261e;
}
}
/* :where(&) reduces the nesting selector to zero specificity,
so the nested declaration is (0,0,0) and loses. */
article {
color: #1c4fa3;
:where(&) {
color: #b3261e;
}
}
/* A parent list takes the strength of its strongest member */
.card, .panel {
&:hover {
border-color: #2f6fed;
}
}How do at-rules behave inside a nested rule?
Conditional at-rules can be nested inside a style rule, and the condition then applies to the parent selector only. The rule .card { @media (min-width: 40rem) { padding: 2rem; } } is the same as putting the media query outside and repeating .card inside it. @supports, @container and @layer can all be nested the same way.
This is the pattern that makes nesting genuinely useful for responsive work, because the breakpoint lives next to the rule it changes rather than in a separate block at the bottom of the file. The one caution is the same as before: keep the plain declarations at the top of the parent rule so the cascade order stays obvious.
.card {
padding: 1rem;
@media (min-width: 40rem) {
padding: 2rem;
}
@container (min-width: 24rem) {
grid-template-columns: 8rem 1fr;
}
}Nesting habits that keep stylesheets readable
- Nesting removes repetition in the selector, not in the cascade. Flattened by hand, a nested rule has the same specificity.
- The ampersand takes the strength of the most specific selector in the parent's list, exactly like
:is(). - Omit the ampersand for descendant relationships; start with a combinator to express a child or sibling relationship.
- Native CSS does not concatenate strings, so
&--wideis not valid. Use&.is-wideor a top-level class. - A nested rule is treated as coming after its parent in the order of appearance, and
:where(&)is the escape hatch when you want the parent to win. - Put the parent declarations first and the nested rules underneath, so the file reads top to bottom in cascade order.
Try it yourself: write a card component twice, once flat and once nested, and confirm in the browser inspector that the computed values are identical. Then change the parent's specificity by swapping .card for a two-class selector and watch the nested rules follow it. Add a nested &:hover and check the specificity of that rule in the inspector, then add the same hover rule flat and compare the two numbers. Finally, try to write &--featured inside the nested block, watch the browser discard it as an invalid selector, and rewrite it as &.is-featured.