Lesson 12
CSS Nesting
If you've used Sass or Less, you already know how nesting works conceptually - writing child selectors inside their parent to keep related styles together. As of 2023, native CSS supports nesting directly in the browser. No build step, no preprocessor, just the language itself.
This lesson covers the syntax, the & selector, nested at-rules, specificity behaviour, and how native nesting differs from Sass. By the end you'll be able to use nesting confidently in production CSS.
The mental model
Nesting is purely a convenience for authoring. It lets you co-locate related rules so you can see the relationship between parent and child styles at a glance. But it doesn't change how the cascade resolves conflicts - the browser de-sugars nested rules into flat equivalents before applying them.
/* Nested */
.card {
padding: 1rem;
& .title {
font-weight: 600;
}
}
/* De-sugars to exactly this: */
.card { padding: 1rem; }
.card .title { font-weight: 600; }
Nesting is syntactic sugar. The browser converts nested rules to flat selectors before resolving styles. Specificity, cascade order, and inheritance all work identically to the flat equivalents. Nesting just makes the code easier to read and maintain.
Basic nesting with &
The & character represents the parent selector - whatever selector wraps the current nesting level. You use it to compose the final selector that the browser will apply:
.card {
background: white;
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem;
/* & .title → .card .title (descendant) */
& .title {
font-weight: 600;
color: var(--accent);
}
/* & .body → .card .body */
& .body {
font-size: 0.85rem;
color: #555;
}
/* &:hover → .card:hover (pseudo-class) */
&:hover {
border-color: var(--accent);
box-shadow: 0 2px 8px rgba(58, 134, 255, 0.1);
}
}
Hover to see the nested &:hover rule in action
&:hover rule firing.When & is required vs optional
The & is sometimes optional and sometimes required. Here's when you need it:
- Required - pseudo-classes and pseudo-elements:
&:hover,&::before - Required - compound selectors (no space):
&.active,&[data-state] - Optional - descendant selectors:
& .childcan be written as just.child { }inside the parent - Required - when placing the parent selector after another selector:
.wrapper &(produces.wrapper .card)
.btn {
/* These two are equivalent for descendant selectors: */
& .icon { margin-right: 0.5rem; }
.icon { margin-right: 0.5rem; }
/* & is REQUIRED for pseudo-classes (starts with :) */
&:hover { background: darkblue; }
/* Without &, the browser can't parse it correctly */
/* & is REQUIRED for compound selectors (no space) */
&.primary { background: blue; }
/* This means .btn.primary, NOT .btn .primary */
/* & AFTER another selector - reverses the relationship */
.dark-mode & { background: #333; }
/* Produces: .dark-mode .btn { background: #333; } */
}
Rule of thumb: If the nested selector should attach directly to the parent (no space between them), you need &. If it's a descendant (space between parent and child), & is optional but improves clarity.
Compound selectors: & with no space
One of the most common uses of & is creating compound selectors - selectors that target the same element with multiple conditions.
Quick refresher: space vs no space in selectors
| Selector | Meaning | Nesting syntax |
|---|---|---|
.card .featured |
Element with class featured inside an element with class card |
& .featured { } |
.card.featured |
Element with both classes card and featured on the same element |
&.featured { } |
.card {
border: 1px solid var(--border);
/* Compound: same element has both .card AND .featured */
&.featured {
border-color: #e76f51;
border-width: 2px;
& .title {
color: #e76f51;
}
}
/* Descendant: .featured is a CHILD element inside .card */
& .featured {
font-style: italic;
}
}
Compound selector demo - the card below has both .nest-card and .featured classes
.featured class on the same element as .nest-card. The compound selector &.featured applies the orange border and title colour.Nesting media queries
One of the most practical benefits of nesting is co-locating responsive styles with the component they affect. Instead of scrolling to a separate @media block at the bottom of your file, you nest the query right inside the selector:
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
@media (min-width: 500px) {
grid-template-columns: 1fr 1fr;
}
@media (min-width: 900px) {
grid-template-columns: 1fr 1fr 1fr;
}
}
/* De-sugars to: */
.grid { display: grid; grid-template-columns: 1fr; gap: 1rem; }
@media (min-width: 500px) { .grid { grid-template-columns: 1fr 1fr; } }
@media (min-width: 900px) { .grid { grid-template-columns: 1fr 1fr 1fr; } }
The nested @media is unwrapped by the browser into a normal media query block wrapping that selector. It's purely co-location for readability - the effect is identical to writing the media query separately.
Resize the browser below 500px to see the grid switch from two columns to one
Co-location is the benefit. Nested media queries don't change behaviour - they de-sugar to the same flat @media blocks. The advantage is purely organisational: all of a component's responsive logic lives together instead of scattered across separate breakpoint sections.
Nesting other at-rules
Media queries aren't the only at-rules you can nest. Container queries (@container) and feature queries (@supports) work the same way:
.sidebar {
width: 100%;
/* Nested container query */
@container (min-width: 300px) {
display: grid;
grid-template-columns: 1fr 1fr;
}
}
.fancy-text {
font-size: 1rem;
/* Nested feature query */
@supports (font-size: clamp(1rem, 2vw, 2rem)) {
font-size: clamp(1rem, 2vw, 2rem);
}
}
/* Both de-sugar to normal at-rule blocks wrapping the selector,
just like nested @media queries. */
Any conditional at-rule can be nested inside a selector. The browser unwraps it into the equivalent flat structure.
Specificity of nested selectors
Nested selectors have exactly the same specificity as their flat equivalents. Nesting does not increase or decrease specificity in any way:
/* Nested */
.card {
& .title { color: blue; }
}
/* Flat equivalent - IDENTICAL specificity (0, 2, 0) */
.card .title { color: blue; }
/* Both have specificity (0, 2, 0) - two class selectors */
However, deep nesting can accidentally create high-specificity selectors without you realising it:
/* Looks innocent... */
.page {
& .section {
& .card {
& .title {
& span {
color: red;
}
}
}
}
}
/* But de-sugars to: */
.page .section .card .title span { color: red; }
/* Specificity: (0, 4, 1) - very hard to override! */
Keep nesting shallow - 2 to 3 levels maximum. Each nesting level adds to the final selector's specificity. Deep nesting creates the same specificity problems that made old-school CSS hard to maintain. Just because you can nest deeply doesn't mean you should.
CSS nesting vs Sass nesting - differences
If you're coming from Sass, native CSS nesting is very similar but has some important differences:
| Feature | Sass | Native CSS |
|---|---|---|
| & for parent reference | Yes | Yes |
| String concatenation with & | &__element produces .block__element |
Not supported - & is a selector, not a string |
| Nesting at-rules | Yes | Yes |
| Descendant without & | .child { } implies descendant |
.child { } also implies descendant (relaxed syntax) |
| Variables | $variables, @mixin, @include | Custom properties only - no mixins |
| Build step required | Yes - compiles to CSS | No - runs directly in the browser |
The biggest difference is that native CSS & cannot do string concatenation. In Sass, &__title inside .card produces .card__title. In native CSS, this doesn't work - & represents the full selector object, not a string you can append to. BEM-style concatenation still requires Sass or manual flat selectors.
/* Sass - works */
.card {
&__title { font-weight: bold; } /* → .card__title */
&--large { font-size: 2rem; } /* → .card--large */
}
/* Native CSS - does NOT work */
.card {
&__title { ... } /* Invalid! & is not a string */
}
/* Native CSS - you'd write these flat instead: */
.card__title { font-weight: bold; }
.card--large { font-size: 2rem; }
Browser support
Native CSS nesting is supported in Chrome 120+, Firefox 117+, Safari 17.2+, and Edge 120+. As of mid-2024, this covers approximately 85% of global users. For older browsers, nested rules are simply ignored (they won't break anything, but the styles won't apply). Consider your audience before relying on nesting in production without a build fallback.
/* Progressive enhancement approach */
/* Flat fallback for older browsers */
.card .title { font-weight: 600; color: var(--accent); }
/* Nested version for modern browsers - won't conflict
since specificity is identical */
.card {
& .title { font-weight: 600; color: var(--accent); }
}
/* Or use @supports if you need conditional logic */
@supports selector(&) {
/* Browser supports nesting */
}
Retrieval check
Question 1
What does & represent in CSS nesting?
Question 2
What is the difference between &.featured and & .featured?
Question 3
Does nesting change specificity compared to the flat equivalent?
Question 4
What does nesting a @media rule inside a selector do?
Question 5
What is a good maximum nesting depth to maintain readable CSS?