Lesson 26 of 30
A plain selector like .card h2 matches every h2 inside a card, no matter how deeply nested — including one that belongs to a completely unrelated component. @scope draws an actual boundary.
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
What's the key difference between native CSS nesting and @scope, given both showed up in CSS around the same time?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Write .card h2 { color: var(--color-accent); } and that rule matches every h2 that's a descendant of .card — one level down, five levels down, it doesn't matter. That's usually what you want, until it isn't: drop a self-contained widget with its own h2 inside a card (a comment count, an embedded preview, anything authored as a separate component) and the card's heading rule reaches in and styles it too, because as far as the selector engine is concerned, "descendant of .card" is the only condition that exists. Nothing about .card h2 knows or cares that the inner h2 belongs to a different component.
@scope fixes this by bounding a rule's reach instead of just describing a relationship. @scope (.card) { h2 { ... } } still matches an h2 that's a descendant of the nearest .card ancestor — but you can also give it a lower boundary: @scope (.card) to (.card-footer) { h2 { ... } } matches inside .card but stops at .card-footer, so anything inside that footer is excluded even though it's technically still nested inside the card. That's a real, native way to say "this rule belongs to this subtree and nothing outside — or beneath — it," without reaching for Shadow DOM or a CSS Modules build step that generates unique class names for you.
It's worth naming the distinction against native CSS nesting directly, since the two are easy to conflate: nesting (covered earlier in this track) is purely a shorthand for writing a selector — .card { .title { ... } } flattens to the exact same global .card .title selector you'd have written by hand, with the exact same unbounded reach. @scope is different in kind, not just in syntax: it genuinely narrows which elements a rule is allowed to match. Nesting changes how you type a selector; @scope changes what the selector is capable of reaching.
@scope (.card) to (.widget) {
h2 {
color: var(--color-accent);
font-weight: 600;
}
}
A .card contains its own h2, plus a nested .widget — an unrelated component — with its own h2.
Without the @scope boundary, a plain .card h2 rule would style both the card's own heading and the heading inside .widget — an unrelated component that just happens to live inside the card's DOM subtree. With @scope (.card) to (.widget), the rule's reach stops at the boundary of .widget: the card's own h2 gets styled, the widget's h2 doesn't, and nobody had to give the widget's heading a defensive, oddly-specific class name just to opt it out of a rule it was never supposed to see in the first place.
@scope solve different problems, even though both showed up in CSS around the same time. Nesting is selector-writing convenience with zero effect on what a rule can match; @scope genuinely limits match reach. A nested rule and a scoped rule can produce completely different runtime behavior even when they look superficially similar.to() boundary excludes descendants of the boundary element, not the boundary element itself's styling in general — it's specifically about where the scoped rule's reach stops, not a statement about the boundary element's own styles from other rules.to() clause, @scope (.card) { ... } still reaches the entire subtree of the nearest .card, including nested unrelated components — the lower boundary is opt-in, not automatic. If you need the widget-exclusion behavior, you have to name the boundary.@scope is an at-rule (not a property-value pair), an unsupported browser doesn't degrade a scoped rule into an unscoped one — it simply ignores the whole block, so nothing inside it applies at all. That means @scope is safest layered on top of styles that already work reasonably without it, not treated as the only thing standing between a component and unwanted global reach.@scope is a clean win; it doesn't retroactively fix a codebase-wide specificity mess on its own."How would you stop a card component's styles from leaking into an unrelated component nested inside it, without Shadow DOM or a CSS-in-JS class-hashing setup?" is a real question probing whether a candidate has moved past "just write a more specific selector" and knows a native scoping primitive exists — and can articulate why nesting alone doesn't already solve this.