An ongoing series on techniques that ship with the browser already - no library, no build step required.
For as long as most people have been writing CSS professionally, nesting selectors has meant reaching for a preprocessor. Sass, Less, whatever the team standardized on years ago - install it, configure a build step, and in exchange get to write .card { p { ... } } instead of typing .card p on every line. That trade stopped being necessary a while back. Every current browser nests selectors natively now, no compiler required.
That part is well covered ground by now. The part worth actually working through is what nesting alone still can’t do, because it’s easy to assume “native nesting” means “everything Sass did, just built in” - and one thing in particular doesn’t carry over: a real boundary. Nest all you like, and a descendant selector still reaches every matching element at any depth, including ones you didn’t mean to touch. Fixing that is a different feature’s job - @scope - and it’s worth knowing both, because they solve adjacent problems that look like the same problem until you hit the one nesting can’t touch.
Here’s both at once. A note component, and something it can contain without a naming convention or a BEM-style class to keep them apart:
Note
Ship the simplest version first.
If a part of it needs a harder caveat, drop one in - it won't inherit this box's styling.
Careful
This paragraph is sitting inside the note above, but it keeps its own look. The note's own paragraph rule never reaches it.
HTML
<div class="note-demo">
<div class="note">
<p class="note-label">Note</p>
<p>Ship the simplest version first.</p>
<p>If a part of it needs a harder caveat, drop one in - it won't inherit this box's styling.</p>
<div class="warning">
<p class="warning-label">Careful</p>
<p>This paragraph is sitting inside the note above, but it keeps its own look. The note's own paragraph rule never reaches it.</p>
</div>
</div>
</div>CSS
@scope (.note) to (.warning) {
& {
border-left: 3px solid var(--accent);
background: var(--color-gray-50);
padding: 1.25rem;
border-radius: 0 8px 8px 0;
}
.note-label {
margin: 0 0 0.4rem;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--accent);
}
p {
margin: 0;
color: var(--color-gray-600);
font-size: 0.9rem;
line-height: 1.6;
& + p {
margin-top: 0.75rem;
}
}
}
.warning {
margin-top: 1rem;
border-left: 3px solid #b91c1c;
background: rgba(185, 28, 28, 0.08);
padding: 1rem 1.25rem;
border-radius: 0 8px 8px 0;
.warning-label {
margin: 0 0 0.4rem;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #b91c1c;
}
p {
margin: 0;
color: #b91c1c;
font-size: 0.9rem;
line-height: 1.6;
font-weight: 600;
}
}The warning box sits inside the note in the markup, but its text doesn’t pick up the note’s paragraph color. Nothing was namespaced by hand to make that true.
Nesting, without the build step
.card {
padding: 1rem;
border: 1px solid var(--color-gray-200);
&:hover {
border-color: var(--color-gray-900);
}
h3 {
margin: 0;
color: var(--color-gray-900);
}
}
Two forms, and they mean different things. h3 { } nested directly, with no &, behaves exactly like Sass always did - it’s shorthand for .card h3, a plain descendant selector. &:hover is explicit: the & stands in for .card itself, so this becomes .card:hover, not a new descendant. You need & any time you’re combining with the parent rather than reaching into its descendants - a pseudo-class, a pseudo-element, or gluing on another class (&.is-active).
That distinction is the one place a habit from Sass can mislead you: Sass used & for the same job, but its & was a text-substitution trick in a compiler that saw the whole selector as a string. Native nesting’s & is a real selector that the browser resolves at parse time, and it’s mandatory in a few more places than people expect - a nested selector that starts with a combinator, for instance, needs the & written out (& > li, not just > li, when you want an explicit child combinator rather than the implicit descendant one).
Nesting collapses at-rules too
The other thing worth knowing, because it has nothing to do with Sass at all: you can nest a media query, a container query, or a @supports block directly inside a rule, instead of repeating the selector inside a separate block elsewhere in the file.
.sidebar {
width: 100%;
@media (min-width: 768px) {
width: 240px;
}
}
Before nesting, that meant writing .sidebar once for the base rule and again inside a @media block somewhere else in the stylesheet - two places to keep in sync every time either one changed. Now it’s one rule, one place, the condition sitting right next to the property it overrides.
What nesting still can’t do
Everything above is still just a shorthand for writing selectors. .card h3 { }, nested or not, compiles down to exactly the same thing: a rule that matches any h3 at any depth inside anything matching .card. Nesting never changes that reach - it just saves you from typing the ancestor part out by hand.
That’s fine until a component can contain another instance of something that looks similar, or a genuinely different component that happens to share a tag name. Drop a card inside a card, an embed inside a post body, a nested comment inside a comment thread, and a plain descendant selector doesn’t know to stop. It was never told where “this component” ends and “whatever it happens to contain” begins - it only knows how to match downward, forever.
Working around that with nesting alone means either scoping every selector down to direct children with > - brittle the moment your markup gets one element deeper than you planned - or reaching for a naming convention like BEM specifically to fence rules in by hand. Both are real solutions. Neither is what nesting itself provides.
@scope draws an actual boundary
@scope (.note) to (.warning) {
p {
color: var(--color-gray-600);
}
}
Read the two selectors after @scope as a donut, not a single line: (.note) is where the scope starts, to (.warning) is where it stops. Anything matching .note becomes a scope root, the p rule applies to paragraphs inside it as usual - and the moment the DOM reaches an element matching .warning, the scope’s reach ends there. Nothing inside the .warning is a “descendant of .note” as far as this rule is concerned, even though it obviously still is one in the actual markup. That’s the whole mechanism the demo above relies on: the note’s p rule is real, it does apply within the note, it just never crosses into the warning because the warning is explicitly the wall.
Drop the to (...) part entirely and @scope (.note) { } still does something useful on its own - it limits a rule to inside .note without needing the class name repeated on every selector, which matters more than it sounds like the moment a stylesheet has several unrelated components that happen to both use a plain p or .label internally. Two components can each say “style my own paragraphs” without either one’s rule leaking into the other’s, and neither has to invent a unique prefix to make that true.
It’s worth being precise that @scope and nesting are solving different problems, not competing for the same one. Nesting is about how you write a selector - saving you from spelling out an ancestor chain by hand. @scope is about how far a selector’s reach extends - a boundary nesting has no concept of at all. The demo above uses both together because that’s the realistic case: nest to keep the note’s own rules readable, scope to make sure they stop where the note actually ends.
Where they stand today
Nesting shipped everywhere first and has had the longer runway:
Browser support · CSS nesting
Support data from MDN’s browser-compat-data project (v8.0.5), sourced from Mozilla. Last checked 2026-07-09.
@scope is close behind in Chrome and Safari, but it’s worth flagging that Firefox shipped it noticeably later than the other two - if a visitor is on an older Firefox release specifically, this is the one to double check before leaning on it for anything load-bearing:
Browser support · @scope
Support data from MDN’s browser-compat-data project (v8.0.5), sourced from Mozilla. Last checked 2026-07-09.
Worth saying plainly: this site’s own stylesheet doesn’t use nesting. It’s one flat global.css file, predating broad support for any of this, and a flat file everyone on the team already knows how to scan hasn’t earned a rewrite just because a newer syntax exists. That’s not a knock on nesting - it’s the same math as any of this site’s other CSS choices: reach for the feature when it’s solving a problem you actually have, not because it’s newer.
Where both genuinely earn a place is exactly the shape of the demo above - a component with internal structure worth nesting for readability, that might one day contain something else that needs to keep its own look. Nest it for how it reads. Scope it for what it can’t accidentally touch.



