CSS Nesting
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.
/* 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 .card | It means | Selects |
|---|---|---|
h2 { } | .card h2 | An <h2> inside a card |
&:hover { } | .card:hover | The card while hovered |
&.featured { } | .card.featured | A card that also has the class featured |
& .featured { } | .card .featured | An element with class featured inside a card |
> p { } | .card > p | A paragraph that is a direct child of a card |
.dark & | .dark .card | A card inside an element with class dark |
& + & { } | .card + .card | A 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.
<!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.
<!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.
/* 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.
& .activeand&.activeselect different things. The space means "inside". - Trying Sass-style name joining.
&__titleand&-largedo 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.cardmeans.card p. &represents the parent selector; use it for states (&:hover), compound classes (&.featured) and context (.dark &).@media,@containerand@supportscan 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.
.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
.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?
A nested selector without & is treated as a descendant of the parent, so it means .nav a.
2. Which nested selector matches a .tab element that also has the class active?
With no space, &.active means .tab.active: the same element with both classes.
3. Inside .card, what does &__body do in native CSS?
Native & stands for a whole selector and does not concatenate text, so write .card__body as its own rule.
4. Inside .btn, why is :hover { } a bug?
Without & the nested selector becomes a descendant selector. Write &:hover to target the button itself.
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
