Inside a CSS @scope block, what does the :scope pseudo-class match, and how does a bare selector such as img behave differently from :scope img?
answer
- the block's rules hang off an implicit prefix
- a descendant is never the element itself
- name the root, don't repeat its class
- the prelude filters but does not weigh
- outside a scope it falls back to the root element
basics
~20 sInside an @scope block, :scope matches the scope root element itself. Bare selectors are implicitly prefixed with :scope and a descendant combinator, so img means :scope img and can never match the root — only elements beneath it.
solid answer
~50 sWithin `@scope (.widget) { ... }`, `:scope` refers to the scoping root — the `.widget` element that opened this scope. Every other selector in the block behaves as if `:scope ` were prefixed with a descendant combinator, so writing `img` means `:scope img`: it matches images inside the widget, never the widget itself, even if the root happened to be an `img`. That is why styling the root requires writing `:scope` explicitly. Two details matter. Writing `.widget` inside the block does *not* select the root — it resolves to `:scope .widget`, a descendant with that class. And the prelude selector contributes nothing to specificity: in `@scope (#app .card) { button { … } }` the button rule's specificity is just that of `button`, not `#app .card button`. Outside any `@scope`, `:scope` in a stylesheet matches the document root element, the same as `:root`.
code
css · 5 lines@scope (.widget) {
:scope { padding: 1rem; border: 1px solid #ddd; }
> p { margin: 0; }
a { color: crimson; }
}go deeper
Remember the one practical rule: to style the scope root itself you write :scope, and every other selector in the block matches only elements inside it.
Explain the implicit :scope-plus-descendant prefix and derive the consequences from it — why a bare selector cannot hit the root, and why repeating the root's class targets a nested instance instead.
Point out that scoping and specificity are decoupled here, and explain why that matters when a codebase has been narrowing rules with long ancestor chains that later became impossible to override.
Be ready to set a house convention: when a component should be scoped versus named, how you keep the low-specificity benefit from being spent immediately, and how you would review scoped CSS so inert root-class rules do not accumulate.
## The implicit prefix The defining behaviour of a scoped style rule is that it is matched relative to a scoping root rather than to the document. The mental model that gets everything right: **every selector inside the block is evaluated as if `:scope ` had been written in front of it, with a descendant combinator.** ```css @scope (.widget) { a { color: crimson; } /* behaves as :scope a */ > p { margin: 0; } /* behaves as :scope > p */ } ``` A relative selector beginning with a combinator, such as `> p`, is anchored to the root explicitly — that is the shape to reach for when you want direct children only. ## Styling the root itself Because the implicit prefix uses a descendant combinator, no bare selector can ever match the scoping root: a descendant of an element is never that element. To style the root, name it: ```css @scope (.widget) { :scope { padding: 1rem; border: 1px solid; } a { color: crimson; } } ``` The common mistake is writing the root's own class inside the block: ```css @scope (.widget) { .widget { padding: 1rem; } /* means :scope .widget — a nested .widget! */ } ``` That rule matches a `.widget` **inside** the widget, which is almost never what was intended. `:scope` is the only spelling that means "this root". ## Specificity The scope root selector in the prelude filters which elements the block's rules may match; it does not add to their specificity. In ```css @scope (#app .card) { button { background: transparent; } } ``` the `button` rule has the specificity of a lone type selector, even though the prelude contains an ID. Compare the pre-`@scope` way of writing the same intent, `#app .card button`, whose specificity is high enough to be painful to override later. This is a real architectural benefit and worth stating in an interview: scoping and specificity are decoupled. You get the narrowing effect of a long ancestor chain without the weight that makes the rule hard to override. When you write `:scope` explicitly it *is* a pseudo-class and counts as one, so `:scope { … }` has the same specificity as a single class, and `:scope a` counts as a pseudo-class plus a type selector. ## :scope outside @scope `:scope` predates `@scope` and is defined generally as "the current scoping root". In a stylesheet with no `@scope` in play there is no explicit scoping root, so it falls back to the document root element — making a bare `:scope` selector equivalent to `:root` there. That fallback is rarely useful in authored CSS, but it explains why `:scope` is not an error outside a scoped block. ## Putting it together ```css @scope (.media) to (.media-content) { :scope { display: grid; gap: 1rem; } /* the .media element */ > img { border-radius: 4px; } /* direct child images */ h2 { margin: 0; } /* any heading in the ring */ } ``` Reading it back: the root gets the grid, direct-child images get corners, headings anywhere in the donut ring get their margin removed — and none of these rules carries the specificity of `.media` at all, so a later utility class can still override any of them. ## Pitfalls worth naming - Writing the root's class instead of `:scope` silently targets a nested instance, and produces a rule that appears to do nothing. - Forgetting that the implicit combinator is *descendant*, not *child*, so `p` reaches arbitrarily deep. Use `> p` when depth matters. - Expecting `:scope` to raise specificity to match the prelude. It does not; if a rule inside the block is losing to something else, the fix is the cascade, not the scope. - Assuming `:scope` inside the block refers to the *nearest* matching ancestor generally. It refers to the root of *this* scope, which for a nested pair of scopes means each block has its own `:scope`.
- Inside @scope (.widget), what does a rule written as .widget { ... } actually select?A `.widget` element that is a descendant of the scoping root, because the rule resolves to `:scope .widget`. For the usual markup where no widget is nested inside another, it matches nothing at all and the rule appears silently inert. Use `:scope` when you mean the root itself.
- Why is it useful that the scope root selector does not contribute specificity?It decouples narrowing from weight. Traditionally you narrowed a rule by prefixing an ancestor chain like `#app .card button`, which also made the rule hard to override later. With `@scope` you get the same restriction on what matches while the rule keeps the low specificity of the selector you actually wrote, so utilities and state classes can still win.
- How do you restrict a scoped rule to direct children of the scope root?Start the selector with a child combinator — `> p { … }` — which anchors it explicitly to the root instead of relying on the implicit descendant prefix. Without the combinator, `p` means `:scope p` and matches paragraphs at any depth inside the scope.
saying these in an interview costs you the question
- Thinks a bare selector inside @scope can also match the scope root
- Writes the root's own class expecting it to style the root
- Assumes the prelude selector adds its specificity to inner rules
- Believes the implicit combinator is child rather than descendant
- Says :scope is invalid outside an @scope block