Nesting lets you write rules inside other rules, so styles for a component and its parts stay together. For years this needed a preprocessor such as Sass. Browsers now support it natively, with a few differences worth knowing.
Basic nesting
.card {
padding: 1.5rem;
border: 1px solid #d9e2ec;
h2 {
margin-top: 0;
font-size: 1.25rem;
}
p {
color: #334e68;
}
}
This is equivalent to writing:
.card {
padding: 1.5rem;
border: 1px solid #d9e2ec;
}
.card h2 {
margin-top: 0;
font-size: 1.25rem;
}
.card p {
color: #334e68;
}
A nested rule without & is treated as a descendant of the parent selector.
The & nesting selector
& stands for the parent selector. Use it when the nested selector must attach directly to the parent, as with pseudo-classes and extra classes:
.button {
background-color: #0b5394;
color: white;
&:hover {
background-color: #073763;
}
&:focus-visible {
outline: 3px solid #f0b429;
outline-offset: 2px;
}
&.button-large {
font-size: 1.25rem;
}
}
That produces .button:hover, .button:focus-visible and .button.button-large.
Without the &, :hover would mean "any hovered descendant", the same as .button :hover, which is almost never what you want.
The space matters. &:hover is the parent being hovered. & :hover (with a space) is any hovered element inside the parent. When a nested pseudo-class rule does nothing, a missing or extra space next to & is the usual cause.
Combinators
You can begin a nested selector with a combinator:
.menu {
> li {
display: inline-block; /* .menu > li */
}
+ .menu {
margin-top: 1rem; /* .menu + .menu */
}
}
& in other positions
& does not have to come first. Putting it later lets a component react to its context:
.card {
background-color: white;
.theme-dark & {
background-color: #102a43; /* .theme-dark .card */
}
:is(.sidebar, .footer) & {
padding: 1rem; /* :is(.sidebar, .footer) .card */
}
}
Nesting media queries and other at-rules
Media queries, @supports, @container and @layer can be nested directly inside a rule. The declarations inside apply to the parent selector:
.layout {
display: grid;
gap: 1rem;
@media (min-width: 48rem) {
grid-template-columns: 16rem 1fr;
}
@supports (grid-template-rows: subgrid) {
grid-template-rows: subgrid;
}
}
The first block is the same as @media (min-width: 48rem) { .layout { grid-template-columns: 16rem 1fr; } }. Keeping a component's breakpoints next to its base styles is one of the most practical benefits of nesting. Media query syntax is covered in the Responsive Web Design course.
Specificity in nested rules
Native nesting treats the parent selector as if it were wrapped in :is(). Usually that makes no difference, but with a selector list it can:
#hero,
.banner {
h2 {
color: white;
}
}
The nested rule behaves like :is(#hero, .banner) h2. As you learnt in the previous lesson, :is() takes the specificity of its most specific argument, so every match scores (1, 0, 1), even h2 elements inside .banner. Sass would output two separate selectors with different specificities.
Avoid mixing IDs and classes in a parent list that you nest inside.
Differences from Sass
| Feature | Sass | Native CSS |
|---|---|---|
| Nest selectors | Yes | Yes |
& for the parent | Yes | Yes |
Concatenate names with &, such as &__title | Yes | No |
| Nested media queries | Yes | Yes |
| Parent list specificity | Each selector kept separate | Combined like :is() |
| Variables, mixins, loops | Yes, at build time | No; use custom properties instead of variables |
The biggest practical difference is concatenation. This is a common Sass pattern for BEM class names:
.card {
&__title {
font-size: 1.25rem;
}
}
In Sass, it produces .card__title. In native CSS, & is a selector, not text, so &__title would mean ".card combined with a type selector __title", which matches nothing. Write the full class name instead:
.card {
padding: 1.5rem;
}
.card__title {
font-size: 1.25rem;
}
If you use a preprocessor or a build tool such as PostCSS or Lightning CSS, it may compile nesting for you, and its rules may differ slightly from the browser's. Know which one is processing your files.
Avoid over-nesting
Deep nesting is easy to write and hard to maintain:
/* Avoid */
.page {
.content {
.article {
.card {
ul {
li {
a {
color: #0b5394;
}
}
}
}
}
}
}
That compiles to a seven-part descendant selector. It is fragile (any change to the HTML structure breaks it), highly specific (hard to override) and hard to read.
Guidelines that keep nesting healthy:
- Nest one level for most things, two at most.
- Nest states (
&:hover), modifiers (&.is-active) and media queries freely; they are the best use. - Do not mirror the HTML tree in your CSS.
- If a nested element needs significant styles of its own, give it a class and a top-level rule.
Browser support
Native nesting is supported in all current major browsers. Early implementations required nested type selectors to start with & or a symbol, so h2 { } directly inside a rule did not work; that limitation was removed in later versions. Check Baseline on web.dev for your audience. Browsers without nesting support drop the nested rules, but they also may fail to parse the rest of the parent rule correctly, so if you support such browsers, use a build tool to flatten nesting rather than shipping it raw.
Debugging nested CSS
DevTools shows nested rules with their full, resolved selector, and displays the parent rule above them. Hover the selector to see its specificity. If a nested rule seems to be ignored, check whether it was treated as a descendant (no &) when you meant to attach it to the parent.
Summary
- Nested rules without
&are descendants;&attaches the selector to the parent. &:hoverand& :hovermean different things; the space matters.- Media queries,
@supportsand@containercan nest inside rules. - Native nesting wraps the parent list in
:is(), and&cannot concatenate class names as Sass does. - Keep nesting shallow: one level usually, two at most.