flashman
← All guides

Encapsulate CSS with @scope

Limit CSS with @scope, a lower boundary, and proximity, then confirm the winning declaration in computed styles instead of trusting source order alone.

2026-10-14 · 8 min read

  • css
  • cascade
  • components

@scope gives a stylesheet a root and, optionally, a lower boundary. Rules inside the block apply only to elements in that region. It is a cascade feature, not a shadow root. Scoped CSS can still see custom properties and inherited values from outside, and the outside can still override it with a more specific or more important declaration.

Use it when a component's selectors should stop at a nested component of the same kind. A card that contains a card should not have to rename every class to .card--outer and .card--inner.

Write the root and the boundary first

The prelude selects the scoping root. @scope (.card) limits the block to each matching card. @scope (.card) to (.card .card) stops the outer card's rules at an inner card. The boundary element and its descendants are outside the outer scope.

:scope inside the block refers to that root. Prefer :scope .title over repeating .card .title, so the same block can be reused if the root selector changes. A lower boundary that matches too high excludes the elements you meant to style. A lower boundary that matches nothing does not exclude them.

  • Name one root selector and one boundary selector.
  • Keep the boundary as specific as the nested component.
  • Do not put @scope only in a source file that the build strips.
  • Check support in the browsers you ship before you remove the old classes.

Let proximity do less work than specificity

Scope proximity is a cascade step after specificity and before source order. When two declarations are otherwise equal, the one attached to the nearer scoping root wins. That is how an inner scope can beat an outer scope without a higher specificity and without depending on file order.

It will not save a low-specificity scoped rule from a high-specificity global rule. Audit those globals before you add @scope, or the new block will look broken in the browser and correct in the file.

Inherited values still cross the boundary

Color, font, and custom properties inherit into the inner component unless the inner component sets them again. @scope does not create a style capsule. If the inner card must be visually independent, set the properties you care about on the inner root.

Shadow DOM remains the tool when you need real encapsulation, including isolation from external selectors. @scope is the tool when you want one document stylesheet with a defined region.

Prove the winner in computed style

For an outer card, an inner card, and a title in each, record which declaration won and why devtools says it won. Repeat after the bundler runs. A minifier that does not understand @scope is a broken build, not a cascade mystery.

Cover a boundary that matches, a boundary that does not match, equal specificity from two roots, and one more specific unscoped rule. The last case should still win.

Read the shipped CSS locally

Use Flashman's CSS formatter on the block, the regex tester to find every @scope and every lower-boundary selector in the built file, diff to compare source with build output, and the color tool when the dispute is which hex was painted.

Keep the root selector and the boundary selector in the component comment. The next edit should not have to infer them from a failing screenshot.

Try these tools