What is @scope?
The @scope at-rule is CSS's native answer to style encapsulation — a problem previously solved only by Shadow DOM, CSS Modules, or naming conventions like BEM. Shadow DOM provides true isolation but creates a hard boundary that makes theming difficult and breaks many familiar CSS patterns. CSS Modules require a build step and generate hashed class names that are invisible in DevTools. @scope takes a different approach: it works with the regular DOM, respects the full cascade, and simply limits which elements a set of rules can match. You define an upper boundary (the scoping root) and an optional lower boundary (the scoping limit), and selectors inside the @scope block only apply to elements between those two boundaries in the DOM tree.
The concept of scope in CSS is not just about preventing style leakage — it also introduces a new dimension to the cascade: proximity. When two scoped rules match the same element with equal specificity, the browser prefers the rule whose scope root is closer to the matched element in the DOM tree. This proximity-based tiebreaker is entirely new to CSS and addresses one of the longest-standing pain points in large stylesheets: distant ancestor selectors overriding styles that logically "belong" to a closer component. With @scope, the component that is structurally nearest to an element gets priority, which aligns CSS behavior with how developers naturally think about component ownership.
Another key feature of @scope is the "donut scope" pattern, enabled by the lower boundary. The syntax @scope (.component) to (.sub-component) creates a ring-shaped scope: styles apply to everything inside .component but stop before reaching .sub-component and its descendants. This is impossible to express with traditional descendant selectors or even the :not() pseudo-class, because those approaches cannot exclude an entire subtree based on its root element. The donut scope is particularly useful in recursive component structures, such as nested accordions, tree views, or comment threads, where you want each level to have its own styling without affecting deeper levels.
How to use this tool
Start by setting the upper boundary in the "Scope Start" field — this is the CSS selector for the element where scoping begins (e.g. .card). Optionally set a lower boundary in "Scope End" to stop styles from cascading into nested subtrees (e.g. .card-footer). Then enter a target selector and a style declaration. Edit the HTML textarea to create the document structure you want to test. The preview panel renders your HTML live and applies the @scope rule, highlighting matched elements. Both code output panels update automatically with copy-ready CSS.
The tool helps you understand the boundaries visually. Elements that match the scoped selector are highlighted in the preview, while elements that fall outside the scope (either above the upper boundary or below the lower boundary) remain unstyled. This makes it easy to verify that your scope boundaries are correct before copying the CSS into your project. Try adjusting the lower boundary to see how the donut scope works in practice, or remove it entirely to see unrestricted scoping.
Practical examples
Component scoping: card styles that don't leak into nested cards
When cards contain other cards (e.g., a dashboard with nested widgets), global styles for .card elements affect every nesting level. @scope lets you style only the top-level card content, excluding nested cards entirely.
@scope (.card) to (.card) {
p {
color: #e2e8f0;
line-height: 1.6;
}
h2 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
img {
border-radius: 8px;
width: 100%;
}
}
Donut scope: style a section but exclude its footer
The donut scope pattern targets everything between two boundaries. Here, all links inside .article are styled, but links inside .article-footer are left untouched — they keep their default or separately defined styles.
@scope (.article) to (.article-footer) {
a {
color: #7c9eff;
text-decoration: underline;
text-underline-offset: 2px;
}
a:hover {
color: #a5b4fc;
}
}
/* Footer links get their own separate styles */
.article-footer a {
color: #94a3b8;
text-decoration: none;
font-size: 0.875rem;
}
Theme region scoping: different styles for sidebar vs main content
Apply entirely different visual treatments to regions of the page without creating extra CSS classes on every element. The sidebar and main content area each get their own scoped theme.
@scope (.sidebar) {
h2 {
font-size: 1rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #94a3b8;
}
p {
font-size: 0.875rem;
color: #cbd5e1;
}
}
@scope (.main-content) {
h2 {
font-size: 2rem;
font-weight: 700;
color: #f8fafc;
}
p {
font-size: 1.125rem;
line-height: 1.75;
color: #e2e8f0;
}
}
Common patterns and best practices
Effective use of @scope follows a few guiding principles that help you avoid common pitfalls and keep your styles maintainable:
- Use
@scopefor component-level styling where you want to avoid class-name prefixing (BEM) but still prevent style leakage. It is particularly effective for design systems where components are composed from generic HTML elements. - Prefer the donut scope pattern (
@scope (.parent) to (.child)) for recursive structures like nested comments, tree views, or accordions. This ensures each nesting level is styled independently. - Keep scope boundaries tied to semantic or structural landmarks in your HTML, not presentational classes. This makes scopes more resilient to markup changes.
- Remember that
@scopedoes not increase specificity — it adds proximity as a new cascade criterion. If you need to override a scoped rule from outside, a higher-specificity selector will still win. - Combine
@scopewith CSS nesting for cleaner, more readable component stylesheets. The two features complement each other well, with@scopeproviding boundary control and nesting providing structural hierarchy. - Avoid deeply nested scope boundaries. If you find yourself nesting more than two levels of
@scope, consider restructuring your HTML or breaking components into smaller, independently scoped pieces.
Browser support
The @scope at-rule is supported in Chrome and Edge since version 118 (October 2023) and in Safari since version 17.4 (March 2024). Firefox has the feature in active development behind a flag. For production use, consider progressive enhancement: write your base styles using traditional selectors and layer @scope on top for browsers that support it. The scoped styles will enhance the experience in supported browsers while the fallback styles keep the layout functional everywhere else.
You can use @supports to detect @scope support and provide conditional styles. However, the most practical approach is to design your scoped styles so that the unsupported fallback (where styles are unscoped and apply globally) is still acceptable. In most cases, the worst that happens without @scope support is that styles "leak" into nested components — the same behavior developers have lived with for decades.
FAQ
How is @scope different from a descendant selector like .card img?
A descendant selector like .card img matches every img inside .card, no matter how deeply nested. If a .card contains a footer with its own images, those images are also selected. @scope solves this by letting you set a lower boundary — for example, @scope (.card) to (.card-footer) ensures that only images between the card root and the footer are targeted. Images inside the footer remain unaffected. This "donut scope" pattern is impossible to express with a plain descendant combinator.
Can I nest @scope rules?
Yes, you can nest @scope rules inside each other. For example, you might write @scope (.page) to (.sidebar) { @scope (.card) to (.card-footer) { img { border: 1px solid red; } } }. The inner @scope further restricts the outer one, so the img rule only applies to images that are both inside a .card (but not its footer) and inside a .page (but not its sidebar). This composability is one of @scope's key advantages over Shadow DOM, which does not allow partial overlap between encapsulation boundaries.
How does @scope interact with specificity?
Selectors inside @scope have their normal specificity, but when two @scope rules match the same element with equal specificity, the rule with the closer (more tightly scoped) scope root wins. This is called proximity-based specificity. It is a new dimension of the cascade introduced by @scope — proximity is evaluated after specificity and before source order. This means that a nearby scope always beats a distant scope when all else is equal, which aligns with how developers intuitively expect component styles to work.
Is @scope the same as Shadow DOM encapsulation?
No, Shadow DOM creates a hard boundary that completely isolates styles. Styles inside a shadow root cannot leak out, and external styles cannot reach in (with limited exceptions via ::part() and CSS custom properties). @scope creates a soft boundary — scoped styles still participate in the global cascade and can be overridden by higher-specificity rules outside the scope. This makes @scope much more flexible for theming and design systems, where you want boundary control without full isolation.
Can I use @scope without a lower boundary?
Yes, @scope (.card) { ... } without a to clause is valid. It scopes styles to everything inside .card elements with no lower limit. The lower boundary is entirely optional. When omitted, the scope extends to all descendants of the scope root, which is useful when you simply want proximity-based cascade behavior without the donut hole. You only need the lower boundary when you want to exclude a specific subtree from the scope.