CSS @scope limits which elements a selector can match to a single DOM subtree, between a scope root and an optional lower boundary, and it adds no specificity of its own. The at-rule became Baseline newly available on 24 March 2026, when Safari 26.4 shipped, so Chrome, Edge, Firefox and Safari all support it now.
Where the scope starts and where it stops
A scope has two edges. The root is inclusive, the limit is exclusive, and everything between them is in scope.
@scope (.article-body) to (figure) {
img {
border: 5px solid black;
background-color: goldenrod;
}
}
Images in the article body get the border. Images inside a <figure> do not, because the limit cuts the subtree off there. OddBird’s explainer for the proposal calls this the ownership problem, and borrows Nicole Sullivan’s description of a scope that “can have a hole in the middle”.
Both edges are adjustable with a child combinator. MDN puts it plainly: “@scope (scope root) to (scope limit > *) would make both bounds inclusive, @scope (scope root > *) to (scope limit) would make both bounds exclusive”.
There is a second form with no prelude at all. Put @scope inside a <style> element in your HTML and it scopes to that element’s parent, which is handy for markup you emit per component or per CMS block.
<section class="promo">
<style>
@scope {
p { color: rebeccapurple; }
}
</style>
<p>Only paragraphs inside this section.</p>
</section>
Why bare selectors inside @scope weigh nothing
This is the part that surprises people who expect scoping to win arguments for them. Inside a scoped block, a bare selector behaves as if :where(:scope) were prepended, and :where() contributes zero. The nesting spec is explicit about the ampersand too: “The & selector behaves like :where(:scope) in @scope rules.”
So the weights come out like this:
@scope (.article-body) {
img { /* 0-0-1 */ }
& img { /* 0-0-1 */ }
:scope img { /* 0-1-1, because :scope is a pseudo-class */ }
}
A global .prose img sits at 0-1-1 and beats the scoped img at 0-0-1, no matter how tightly you drew the scope. To hold ground against the rest of the stylesheet you still need cascade layers, or an explicit :scope in the selector to buy back the missing 0-1-0.
When did CSS @scope become Baseline?
Chrome and Edge have had the at-rule since version 118, and Safari since 17.4. Firefox was last, landing it in Firefox 146 on 9 December 2025: “The @scope rule is now supported, allowing authors to restrict styling to a subtree of the DOM.”
That should have made it Baseline in December. It did not, for two reasons that are worth knowing.
Safari 26.0 regressed. Between 26.0 and 26.3, MDN’s compatibility data records the implementation as partial: “CSS rules within @scope are not applied to <input> and <textarea> elements.” Safari 26.4, on 24 March 2026, fixed it. If you style form controls from inside a scoped block, that is a real bug you can still hit on machines a version or two behind.
The other reason is that the web-features definition of @scope bundles more than the at-rule. It also covers CSSScopeRule in the CSSOM and the &-as-:where(:scope) rule, and that last one arrived separately: Chrome 143, Firefox 142, Safari 26.2. Baseline waits for the slowest piece. Widely available is projected for September 2028.
An engine that does not know @scope drops the whole block, so everything inside it is an enhancement by default. For the JavaScript side, the CSSOM interface is the detection hook.
if ("CSSScopeRule" in window) {
document.documentElement.classList.add("has-scope");
}
Be honest about what that test covers. Safari 26.0 through 26.3 pass it and still fail on inputs.
Does @scope replace CSS Modules or shadow DOM?
No, and conflating them causes most of the disappointment. @scope gives you selector isolation, not style isolation. Inherited properties such as color and font-family cross the limit and carry on down into the donut hole, exactly as they always have.
Shadow DOM is the opposite trade. Nothing gets in or out unless you ask for it, which suits a widget dropped into a page you do not control. Everywhere else it fights you, and the explainer is blunt about the reason: shadow DOM “is not designed for the common ‘design system’ approach, where patterns overlap in more fluid ways”. If you are weighing that boundary up, our notes on what breaks when you put web components in React cover the encapsulation side in more detail.
CSS Modules and Vue’s scoped styles solve the naming half at build time by rewriting class names or adding attributes. @scope does the same job without a build step, and adds two things those tools cannot express: a lower boundary, and proximity. If your project already hashes class names, those two are the only reasons to reach for it.
How proximity decides a theme conflict
The cascade grew a step. Importance, then layers, then specificity, then scope proximity, then source order. Proximity means the fewest hops up the tree from the matched element to its scope root.
@scope (.dark-theme) {
a { color: plum; }
}
@scope (.light-theme) {
a { color: purple; }
}
Nest a .light-theme block inside a .dark-theme one and the links inside it come out purple, whichever order the blocks appear in the stylesheet. Write the same two rules as .dark-theme a and .light-theme a and you are back to source order deciding, which breaks the moment someone reorders an import.
Proximity sits below specificity on purpose. The explainer calls it weak proximity, and it only settles ties. Give one of those two rules an extra class and proximity never gets a turn.
What I would ship today
Use it where the boundary is the point: a CMS article body that needs a hole cut round embeds and figures, a comment thread, a third-party block you style around. Use it for nested themes, where proximity replaces a fragile ordering convention with a structural one. Keep CSS Modules or BEM where you already have them, because swapping a working naming scheme for an at-rule that does not raise specificity buys you nothing. If a component has to survive a hostile stylesheet, that is still shadow DOM.
Given the March 2026 Baseline date and the form-control bug in Safari 26.0 to 26.3, no component’s styles should live only inside a scoped block yet. Write the base rules unscoped, keep the boundary-dependent ones in @scope, and an engine that drops the block leaves you with a leakier stylesheet instead of an unstyled component. If you are reorganising a stylesheet around components anyway, moving breakpoints into container queries will change more than this at-rule does.
Whoooop builds and maintains front ends where the stylesheet still has to be predictable after several years and several developers. Untangling one that has stopped being predictable is part of our responsive front-end work.