skip to content

Scoping and Encapsulation (@scope)

Native ways to stop styles leaking, now that @scope gives CSS a real boundary with its donut-shaped lower limit. A good discussion question about why CSS historically had no module system and what each workaround actually costs.

part ofCSSoverview, primer and where to startread it →
on this pageshow

questions

5

In CSS, why is a class rule such as .title { color: red } described as living in a global namespace, and what problems does that create in a large codebase?

level: juniorimportance: must knowfreq 55%

answer

  1. one document, one name pool
  2. the file a rule lives in is irrelevant
  3. a collision raises no error
  4. three leaks: collision, reach-in, inheritance
  5. boundaries come from convention, build, DOM or @scope

basics

~20 s

CSS selectors are matched against the entire document regardless of which file they were written in, so one .title rule styles every element with that class. Collisions are silent, and unrelated components end up restyling each other.

solid answer

~40 s

CSS has a single global name pool. A selector is not bound to the file it was written in — the browser merges every stylesheet into one cascade and matches every selector against the whole document. So `.title` written for a card also matches a `.title` in the footer, and a rule like `.sidebar a` reaches into any component that happens to sit inside `.sidebar`. Nothing errors; the element simply gets whichever declaration wins on origin, importance, layer, specificity and order. That is why large codebases add a boundary from outside the language: a naming convention so names cannot collide, a build step that rewrites class names into unique ones, a shadow root, or `@scope` to limit which elements a block of rules may match. Plain CSS gives you no privacy by default.

go deeper

for a junior

Be ready to say plainly that CSS selectors are global — the file a rule sits in has no effect on what it matches — and give one concrete collision, such as two components each defining .title.

for a middle

Explain the three distinct leaks: colliding names, an ancestor selector reaching into a subtree, and inherited properties flowing down. Note that none of them produce an error and that each needs a different fix.

for a senior

Show you can diagnose a leak in a real codebase — how you find which rule won and why simply overriding it is risky when you cannot see every element that selector matches elsewhere.

for a principal

Own the tradeoff between conventions people must remember and boundaries tooling enforces, and be able to say what a chosen scoping strategy costs in theming flexibility, migration effort and onboarding.

## Everything lands in one pile When a page loads, the browser gathers the rules from every `<style>` element, every linked stylesheet and every `@import`ed file into one collection and applies them to one document tree. The file a rule came from is not part of how it is matched. A selector describes a shape of element — "an element with class `title`" — and every element in the document that fits that shape is styled, wherever it lives and whoever wrote it. That is what "global namespace" means here. In a language like JavaScript, a name declared in a module is invisible outside it unless exported. CSS has no equivalent: there is no import, no export, no privacy. Two files can declare the same class name and neither is aware of the other. ```css /* card.css */ .title { font-size: 1.25rem; color: #222; } /* footer.css, written by another team */ .title { font-size: 0.8rem; text-transform: uppercase; } ``` Both rules apply to every `.title` in the page. No warning is produced. The card's title quietly picks up the uppercase transform, and its font size depends on which file the bundler happened to emit last — because the two selectors have identical specificity, so order of appearance decides. ## Three different leaks, often confused **Name collision.** Two components choose the same class name for different things, as above. The winner is decided by the cascade, not by intent. **A selector reaching in.** A rule such as `.sidebar a { color: rebeccapurple; }` is not a collision at all — the author never touched the component's names. It matches any link that happens to be a descendant of `.sidebar`, including links inside a component that was dropped there later. Renaming the component's classes does nothing about this, because the selector never mentioned them. **Inheritance.** Inherited properties such as `color`, `font-family`, `line-height` and `visibility` flow down the tree from ancestors regardless of any selector. A component that never sets `color` will take whatever colour its container established. This is by design and is not a bug, but it is often reported as one. These three need separating because the fixes differ. Naming discipline addresses only the first. Most scoping mechanisms address the first and part of the second. Nothing except explicitly declaring a value stops the third. ## Why CSS was built this way The global cascade is not an oversight. It is the mechanism that lets one line of CSS restyle a whole site: set a font on `body` and the document follows; declare a design token once and every component reads it. The same property that makes a stylesheet leak is the property that makes it powerful. Any scoping strategy trades some of that reach for predictability, and that trade is the real subject of the discussion. ## The classes of fix Four broadly different answers exist, and they operate at different points in the stack. 1. **Convention.** Agree that every class name carries the component's name as a prefix, so collisions become impossible by construction. Costs nothing at runtime; enforced only by discipline and review. 2. **Build-time renaming.** A tool rewrites authored class names into unique generated ones, so two components can both write `.title` and never meet. The compiler enforces what a convention only asks for — but only for the names it rewrites. 3. **Shadow DOM.** The component's markup lives in a separate tree with its own style boundary: outside selectors do not match inside it and inside selectors do not match outside. This is the only one of the four that blocks a foreign selector from reaching in. Inherited properties and custom properties still cross the boundary. 4. **`@scope`.** A CSS at-rule that limits which elements the rules inside it can match — to a subtree, optionally with a hole cut out of it. It keeps your rules in; it does not keep other people's rules out. ## What "scoped" does and does not promise No mechanism here makes a component's styling absolute. Even inside a shadow root, inherited values arrive from outside and custom properties are deliberately pierceable — that is how theming works at all. And none of these change the cascade's tiebreakers: within whatever set of rules does match, origin and importance, layers, specificity and order still decide the winner. The practical takeaway for an interview is the framing: CSS has no module system, leakage is the default rather than a failure, and every solution is a boundary imposed from outside the plain language — by a naming rule, a build step, a DOM boundary, or an at-rule.

  • If two stylesheets define the same selector with the same specificity, what actually decides which declaration applies?
    The cascade, in order: origin and importance first, then cascade layers, then specificity, then order of appearance. With identical selectors in the same origin and layer, the one that appears later in document order wins — which in a bundled app means whichever file the build happened to emit last, an arbitrary and unstable answer.
  • Does renaming a component's classes to unique names protect it from all outside styling?
    No. It removes name collisions, but a selector that never mentions those names still matches — `.sidebar a`, `section > p`, or `* { box-sizing: border-box }` all reach in regardless of what the classes are called. Inherited properties such as `color` and `font-family` also still flow down from ancestors.
  • Why does a global cascade remain desirable even though it causes leakage?
    Because reach is the point. Setting typography on `body`, defining tokens once at the root, or shipping a reset all depend on one rule affecting the whole document. Encapsulation buys predictability at the price of that reach, so the sensible design scopes component internals while deliberately leaving a global layer for tokens, resets and base typography.

saying these in an interview costs you the question

  • Thinks a stylesheet only applies to the component that imported it
  • Believes duplicate selectors produce an error or warning
  • Says higher specificity is what makes a component safe
  • Thinks separate CSS files create separate scopes
  • Claims !important prevents other stylesheets from leaking in

context

open as a page

In CSS, what does the optional to (...) limit on the @scope at-rule do, and why is a rule such as @scope (.card) to (.card-body) called donut scoping?

level: middleimportance: should knowfreq 30%

basics

~20 s

The to (...) selector marks a lower boundary: rules inside the @scope block match the scope root and its descendants, but stop at any element matching the limit, that element included. The styled region is a subtree with a hole, hence donut.

open as a page

For a CSS component library shared by several teams, how would you choose between @scope, build-time class-name hashing, and shadow DOM as the style-encapsulation strategy, and what does each actually guarantee?

level: principalimportance: should knowfreq 26%

basics

~20 s

Hashing guarantees your class names cannot collide with anyone else's. @scope guarantees your rules only match inside a chosen subtree. Only shadow DOM guarantees outside selectors cannot reach in — and none of them stop inherited properties from crossing.

open as a page

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?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

Inside 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.

open as a page

In the CSS cascade, what is scope proximity, at what point is it consulted, and what does it decide?

level: seniorimportance: nice to knowfreq 16%

basics

~20 s

Scope proximity is a cascade tiebreaker for declarations coming from @scope blocks: the one whose scoping root is fewer generational hops from the matched element wins. It is consulted after specificity and before order of appearance.

open as a page