Design · · 6 min read
@scope is Baseline: scoped CSS without the class-name gymnastics
@scope became Baseline newly available in March 2026. The donut, where scope proximity really sits in the cascade, and why it is not nesting.
@scope limits a block of CSS to a subtree of the document, with an optional lower boundary:
@scope (.card) to (.card__body) {
a { color: var(--card-link); }
}That rule matches links inside .card, but stops at anything inside .card__body. No extra classes, no build step, no shadow DOM.
It became Baseline newly available on 24 March 2026, when Safari 26.4 fixed the last gap. This covers the syntax, the part of the cascade it changes, and where it differs from CSS nesting.
The syntax
@scope (<scoping root>) to (<scoping limit>) {
/* scoped style rules */
}Both halves are optional. @scope (.card) has no lower boundary; @scope to (.card__body) sets only a boundary, using the implicit root described below.
An element is in scope when it is an inclusive descendant of the scoping root and not an inclusive descendant of a scoping limit. "Inclusive" in both directions is what makes the donut work: the root itself is in scope, and the limit element itself is out.
Selectors inside the block are relative selectors with the scoping root and a descendant combinator implied. These two rules match the same elements:
@scope (#my-component) {
p { color: green; }
:scope p { color: green; }
}Add a combinator to change the relationship:
@scope (#my-component) {
> p { color: green; } /* direct children only */
}Pseudo-elements cannot be a scoping root or a scoping limit. They are invalid in both positions.
The scoping root you get for free
Leave out the root entirely and the scoping root becomes the parent element of the stylesheet's owner node — for a <style> element, whatever contains it. The spec's own example:
<div>
<style>
@scope {
p { color: red; }
}
</style>
<p>this is red</p>
</div>
<p>not red</p>That is genuinely useful for server-rendered components and for CMS blocks that ship a small chunk of CSS with their markup. If there is no parent element and the rule is in a shadow tree, the shadow host becomes the root.
Limits that depend on where the root is
Scoping limits are matched among the root's descendants, and they can use :scope to require a relationship:
/* .content is only a limit when it is a direct child of the root */
@scope (.media-object) to (:scope > .content) { … }:scope and & are not the same thing
Both match the scoping root. They have different specificity:
| Selector | Specificity |
|---|---|
:scope | 0,1,0 |
& | 0,0,0 |
Inside @scope, & is defined to behave as :where(:scope), which is where the zero comes from. Use & when you want to name the root without adding weight, and :scope when you want a class-level selector.
Where scope proximity actually sits in the cascade
@scope adds a step to the cascade, and it is commonly described as "proximity beats specificity". It does not. The cascade sorts declarations in this order of precedence:
- Origin and importance
- Context (shadow tree encapsulation)
- The style attribute
- Cascade layers
- Specificity
- Scope proximity
- Order of appearance
Proximity is a tie-breaker that runs after specificity and before source order. The spec's own prose about @scope says proximity applies "regardless of specificity or order of appearance", but the normative sorting order above puts it below specificity — that is the list to trust. So between two declarations of equal specificity from different scopes, the one whose scoping root is fewer generational or sibling hops from the matched element wins — regardless of which came later in the stylesheet. A rule with no scoping root at all counts as infinitely far away, so any scoped rule of equal specificity beats it.
This is the practical win. The classic light/dark nesting problem:
@scope (.light) {
a { color: darkmagenta; }
}
@scope (.dark) {
a { color: plum; }
}A .dark panel inside a .light page gets plum, and a .light panel inside a .dark page gets darkmagenta, without !important, without extra specificity, and without caring about the order of the two blocks.
The prelude does not add specificity
This is the difference from nesting that catches people. A scoped selector does not inherit the specificity of the @scope prelude. The spec's example: these two have the same specificity, 0,0,1.
@scope (#hero) {
img { border-radius: 50%; }
}
:where(#hero) img { border-radius: 50%; }Write the same thing with nesting and the ID counts:
#hero {
img { border-radius: 50%; } /* specificity 1,0,1 */
}So @scope gives you containment without an escalating specificity war, which is exactly what the class-name conventions were invented to work around. What you lose is the nesting shorthand's convenience; the two features are complementary, not alternatives.
Browser support
From MDN's browser-compat-data at the time of writing.
| Browser | @scope |
|---|---|
| Chrome / Edge | 118 (October 2023) |
| Firefox | 146 (December 2025) |
| Safari | 17.4 (March 2024), with a regression in 26–26.3, fixed in 26.4 (March 2026) |
The Safari history is worth a moment if you check support programmatically. Safari shipped @scope in 17.4, then in Safari 26 the implementation stopped applying scoped rules to <input> and <textarea> elements — recorded in browser-compat-data as a partial implementation for 26 through 26.3. Safari 26.4 fixed it, which is why the Baseline date is March 2026 and not March 2024.
If you support Safari 26 to 26.3, styling form controls from inside @scope will not work there. Everything else will.
@supports cannot test at-rules, and CSS.supports("selector(:scope)") is no help — :scope has worked on its own for years and says nothing about @scope. The reliable check is the CSSOM interface, which browser-compat-data records as shipping in exactly the same versions as the at-rule:
const hasScope = "CSSScopeRule" in window;Better still, do not detect it. An unsupported @scope block is dropped in its entirety, so put the styles that must always apply outside the block and use @scope for the scoped refinement.
A related piece of the spec, @import … scope(…), scopes the rules of an imported stylesheet without wrapping them. Browser-compat-data records no support for it in any engine at the time of writing.
FAQ
Does @scope replace BEM?
It replaces the part of BEM that exists to stop styles leaking — the long prefixed class names. It does not replace the part that makes markup self-describing. Many teams will keep readable class names and drop the defensive prefixing.
Is it a replacement for shadow DOM?
No. Shadow DOM is a persistent one-to-one relationship between a host and its tree, and it stops outside selectors from matching inside it at all. @scope only limits which elements a rule matches: page styles still reach into a scope, and overlapping scopes can cover the same elements. Use shadow DOM when you need encapsulation you can rely on, and @scope when you want to keep a stylesheet tidy.
Can I use it with CSS nesting inside?
Yes. Rules inside @scope nest as usual, and & inside a nested rule refers to the nested parent, not the scoping root.
What is the performance cost?
There is no public cross-engine benchmark to cite, so treat any number you read with suspicion. Structurally, a scoped rule adds an ancestor check on the subject and, for limits, a second one — comparable to what a descendant selector already does.
Sources
- CSS Cascading and Inheritance Module Level 6 — cascade sorting order,
@scopesyntax, scoping roots and limits; read from its source in the csswg-drafts repository - mdn/browser-compat-data — support versions and release dates for
css.at-rules.scope - web-platform-dx/web-features — Baseline status and date for the
@scopefeature
More to read
- Tech
Tech · · 6 min read
Highlighting text without touching the DOM: the Custom Highlight API
Style arbitrary text ranges with CSS and no wrapper elements. The short list of properties that apply, live versus static ranges, and the overlap rules.

Design · · 8 min read
Two engines now style the real <select>. Here is how it works
Safari 27 shipped appearance: base-select on 14 September 2026, so Chrome and Safari both style the native dropdown. The parts, the rules and the traps.