Isolation, donut scope and the :scope pseudo-class
Style isolation used to be the exclusive territory of Shadow DOM and CSS-in-JS. With CSS @scope, the native cascade brings real boundaries into regular stylesheets, with no JavaScript and no custom elements required. Donut scope, proximal specificity and the :scope pseudo-class make CSS components more maintainable and free of unwanted overlap.
Table of Contents
- 1. The problem with global stylesheets
- 2. CSS @scope: syntax and core principle
- 3. Using the :scope pseudo-class correctly
- 4. Donut scope: carving out exceptions
- 5. Proximal specificity: a new cascade rule
- 6. @scope in component-based projects
- 7. Inline @scope with <style> in HTML
- 8. @scope vs. Shadow DOM vs. BEM: a comparison
- 9. Browser support and progressive enhancement
- 10. Summary
- 11. FAQ
1. The problem with global stylesheets
Anyone who has worked on large web projects for a while knows the phenomenon: a stylesheet grows, selectors get more and more specific, and eventually a rule at spot A unexpectedly overwrites the appearance at spot B. The global nature of CSS is at once its strength and its biggest source of maintenance problems. Classic approaches such as BEM, CSS Modules or CSS-in-JS solve the problem through naming conventions or runtime injection, but none of them is a native CSS mechanism. CSS @scope is the cascade's first real step toward genuine, declarative style isolation directly inside the stylesheet.
The goal of CSS @scope is not to replace CSS Modules or Shadow DOM. It is about turning a specific DOM subtree into the explicit scope of a set of CSS rules. Inside that scope the defined rules apply, outside it they do not, without forcing class naming conventions or requiring JavaScript. That sounds simple, but it fundamentally changes how you think about component styles.
2. CSS @scope: syntax and core principle
The CSS @scope rule follows the at-rule syntax and takes a selector for the scoping root node, the so-called scope root, as its first parameter. Everything inside the @scope (.card) { ... } block applies only to elements located within an element carrying the class .card. The crucial difference from an ordinary descendant selector lies in the second parameter, the scope limit, which is covered in section 4. A plain @scope without a limit initially behaves like a nested selector, but with one important twist: proximal specificity.
The syntax reads: @scope (<scope-start>) [to (<scope-end>)] { ... }. The scope root is determined by the selector inside the parentheses after @scope. The optional to (...) defines a scope limit: elements matching that selector, and their children, are excluded from the scope. Inside the block you can use all normal CSS rules, including pseudo-classes, media queries and other at-rules. That makes CSS @scope a full-fledged containment mechanism for stylesheets.
/* @scope basics - isolate card component styles */
@scope (.card) {
/* :scope refers to the scope root element (.card itself) */
:scope {
background: white;
border-radius: 0.75rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
padding: 1.5rem;
}
/* These selectors only match inside .card - no leaking */
h2 {
font-size: 1.25rem;
font-weight: 700;
color: #1e1b4b;
margin-block-end: 0.5rem;
}
p {
color: #4b5563;
line-height: 1.6;
}
.badge {
display: inline-flex;
align-items: center;
background: #ede9fe;
color: #4a1d96;
border-radius: 9999px;
padding: 0.2em 0.75em;
font-size: 0.75rem;
font-weight: 600;
}
}
/* Outer h2 elements are NOT affected - true isolation */
h2 {
font-size: 2rem;
color: #0f172a;
}
CSS @scope does not change the specificity of the selectors inside the block: h2 inside @scope (.card) still has a specificity of 0-0-1, not 0-1-1 as with .card h2. Instead, the spec introduces a new concept: proximity. Whichever scope sits closest to the element in the DOM hierarchy wins when specificity is equal. That is a fundamentally different tie-breaking behavior than plain source order in the stylesheet.
3. Using the :scope pseudo-class correctly
Inside an @scope block, the :scope pseudo-class refers to the scope root itself, the element matching the scope start selector. Outside @scope, :scope behaves like :root in a stylesheet context, so it is rarely useful there. In JavaScript, on the other hand, :scope has long been used with querySelector: element.querySelector(':scope > .child') limits the search to direct children of the calling element.
Inside an @scope context, :scope is essential whenever you want to apply styles to the root node itself. If you write only .card { ... } inside an @scope (.card) block, it would also match any nested .card elements within the scope. With :scope { ... } you target exclusively the scope root, the outermost .card element. This matters most for recursive components or widgets that can nest inside themselves, such as comment threads or cards containing sub-cards.
/* :scope pseudo-class - targets only the scope root, not nested instances */
@scope (.comment) {
/* Apply padding/border only to the outermost .comment */
:scope {
padding: 1rem;
border-left: 3px solid #7c3aed;
margin-block-start: 1rem;
}
/* .author inside ANY .comment in scope - including nested ones */
.author {
font-weight: 700;
color: #4a1d96;
font-size: 0.875rem;
}
/* Only direct child replies of THIS scope root */
:scope > .replies {
margin-inline-start: 1.5rem;
border-inline-start: 2px solid #ede9fe;
padding-inline-start: 1rem;
}
}
/* :scope in JavaScript querySelector - same concept, pre-dates @scope */
/* const items = listEl.querySelectorAll(':scope > li'); */
/* Only direct <li> children of listEl, not deeper descendants */
4. Donut scope: carving out exceptions
The so-called donut scope is one of the most powerful concepts in CSS @scope. With the to (...) syntax you can define a scope limit, a selector whose matching elements and all their descendants are excluded from the scope. The result is a scope shaped like a donut, with a hole in the middle. A typical use case: a .theme-dark component should apply dark styling to all its child nodes, but any embedded .theme-light components should keep their own light styling. Without donut scope, you would have to solve that with escalated specificity or !important, both maintenance-unfriendly approaches.
The scope limit operates on DOM depth: as soon as a child node matches the to selector, that node and its entire descendant tree are carved out of the scope. The boundary is exclusive, meaning the limiting element itself is excluded from the scope. You can specify multiple selectors as the limit by separating them with a comma. That makes donut scope a precise tool for theme switching, slot-based layouts and components with exception zones.
/* Donut scope - exclude nested theme islands from the outer scope */
@scope (.theme-dark) to (.theme-light, .theme-reset) {
/* Only applies inside .theme-dark, but NOT inside nested .theme-light */
:scope {
background: #0f172a;
color: #e2e8f0;
}
a {
color: #c4b5fd;
text-decoration-color: rgba(196, 181, 253, 0.4);
}
button {
background: #4a1d96;
color: #ede9fe;
border: 1px solid rgba(196, 181, 253, 0.3);
}
h2, h3 {
color: #f8fafc;
}
}
/* Nested .theme-light keeps its own rules - donut scope excludes it */
@scope (.theme-light) to (.theme-dark) {
:scope {
background: #ffffff;
color: #0f172a;
}
a { color: #4a1d96; }
button { background: #ede9fe; color: #4a1d96; }
}
5. Proximal specificity: a new cascade rule
Proximal specificity is the conceptually new tie-breaking mechanism that CSS @scope introduces into the cascade. Until now, the rule was: when specificity is equal, the rule that appears later in the stylesheet wins. With CSS @scope, a third criterion enters the picture: which scope root sits closest to the element in question within the DOM hierarchy? An inner scope beats an outer scope when both have the same specificity. This enables natural theme overriding without a specificity arms race.
Proximal specificity only kicks in between rules from different @scope contexts. Within the same scope, classic cascade logic still applies: specificity first, then source order in the stylesheet. The new logic solves the "specificity arms race" problem: if a parent component styles a heading with .card h2, and a child component wants to style that same heading differently with plain h2, the child rule loses despite its logical proximity. With CSS @scope and proximal specificity, the scope closer to the element wins, a behavior that matches developer intuition.
6. @scope in component-based projects
In projects built on component libraries, whether Hyva blocks in Magento, React, Vue or plain HTML templating, CSS @scope offers an alternative to framework-level scoped-CSS solutions. Instead of enforcing a dedicated class naming convention for every component or configuring CSS Modules, you can group styles inside an @scope block that uses the component root selector as its scope root. This approach is portable: it works in any browser that supports @scope, with no build tools or transpilers needed.
A proven strategy is to define a dedicated CSS @scope block per component and encapsulate all component-specific rules inside it. Global styles, typography basics, color variables, layout utility classes, stay outside every scope and keep applying everywhere. This separation makes the stylesheet explicit: whatever is inside belongs to the component, whatever is outside is global. For teams, that clarity is often more valuable than the technical isolation itself.
/* Component-scoped styles for a product card - Hyva / Magento context */
@scope (.product-card) {
:scope {
display: grid;
grid-template-rows: auto 1fr auto;
border-radius: 1rem;
overflow: hidden;
background: white;
box-shadow: 0 2px 8px rgba(74, 29, 150, 0.08);
transition: box-shadow 0.2s ease;
}
:scope:hover {
box-shadow: 0 8px 24px rgba(74, 29, 150, 0.18);
}
.product-name {
font-weight: 700;
font-size: 1rem;
color: #1e1b4b;
margin-block: 0.75rem 0.25rem;
padding-inline: 1rem;
}
.product-price {
font-size: 1.25rem;
font-weight: 800;
color: #4a1d96;
padding-inline: 1rem;
}
.add-to-cart {
display: block;
width: 100%;
background: #7c3aed;
color: white;
font-weight: 600;
padding: 0.75rem 1rem;
border: none;
cursor: pointer;
transition: background 0.2s ease;
}
.add-to-cart:hover {
background: #4a1d96;
}
}
7. Inline @scope with <style> in HTML
A particularly interesting application of CSS @scope is using it inside inline <style> tags directly in the HTML document, combined with the future-facing <style scoped> approach. Even today you can write an @scope block without a scope root selector in a <style> tag inside the <body>: @scope { ... }. In that case the scope root is the nearest parent element of the <style> tag. That enables genuine single-file components in plain HTML, a technique that is especially interesting for server-side rendering contexts where no build pipeline exists.
This technique has limitations: inline styles in the body are render-blocking and can cause performance problems if used at scale. It is better suited to critical above-the-fold styles or dynamically generated content, where the stylesheet is tightly coupled to the HTML fragment. In a Magento context with Hyva, this approach fits well for widget blocks that ship with their own <style> block, provided the CSP header is configured accordingly.
8. @scope vs. Shadow DOM vs. BEM: a comparison
The three most important approaches to style isolation solve the same problem at different levels and with different trade-offs. CSS @scope works at the stylesheet level and is declarative: no JavaScript, no build step, no custom element required. Shadow DOM isolates completely: styles from the light DOM do not leak into the shadow DOM and vice versa. That is stronger, but it forces the use of custom elements. BEM is a convention with no real technical isolation: if you do not follow it, the encapsulation breaks.
| Criterion | CSS @scope | Shadow DOM | BEM / CSS Modules |
|---|---|---|---|
| Technical isolation | Partial (cascade) | Complete | Convention-based |
| JavaScript required | No | Yes (custom elements) | No (BEM) / Build (Modules) |
| Global styles inherited | Yes | No (except custom properties) | Yes |
| Donut pattern possible | Yes (to syntax) | Only via slots | Not native |
| Browser support (2026) | Chrome 118+, Safari 17.4+ | All modern browsers | All browsers |
The choice between the three approaches depends on context: CSS @scope is ideal for CSS-first projects where you do not want to introduce custom elements but still want to organize styles per component. Shadow DOM is the right choice when true DOM isolation is required, for example for reusable web components in a design system. BEM and CSS Modules remain relevant wherever browser support for @scope is not yet sufficient or teams do not want to break existing conventions.
9. Browser support and progressive enhancement
Starting with Chrome 118 (October 2023) and Safari 17.4 (March 2024), CSS @scope is available in the major browsers. Firefox is under active implementation at the time of this article (May 2026). For production use, a progressive enhancement strategy is recommended: styles that work correctly even without @scope form the baseline. The @scope block adds isolation as an enhancement. Since CSS @scope does not break any existing rendering, non-supporting browsers simply ignore the block, this approach carries low risk.
Detection happens via @supports selector(:scope) or via feature detection in JavaScript through CSS.supports('@scope (.x) { .y {} }'). In practice that means: define critical layout and typography rules outside of @scope blocks, and add isolation as an enhancement. For Tailwind CSS projects, a combination works well: utility classes outside, component-specific semantic styles inside CSS @scope blocks.
10. Summary
CSS @scope is a native cascade mechanism that enables style isolation without Shadow DOM, without JavaScript and without build tools. The core concepts are: the scope root as the starting point of applicability, the scope limit for donut scope, the :scope pseudo-class for the root node itself, and proximal specificity as a new tie-breaking criterion in the cascade. Together, these building blocks make CSS components noticeably more maintainable, without giving up global inheritance.
The biggest practical win lies in explicit organization: a CSS @scope block clearly communicates which rules belong to which component. No namespacing, no build step, no convention enforcement. For teams working CSS-first and supporting modern browsers, CSS @scope is a tool that noticeably simplifies day-to-day stylesheet work, especially in component-based Magento/Hyva projects with many reusable UI elements.
CSS @scope: the essentials at a glance
Syntax
@scope (.root) to (.limit) { :scope { } .child { } }: the scope root and optional scope limit define the applicability range.
Donut scope
With to (.exception), embedded elements are excluded from the scope. Ideal for theme switching without a specificity arms race.
:scope pseudo-class
Inside @scope, this refers to the scope root itself, which matters for recursive components.
Browser support
Chrome 118+, Safari 17.4+. Progressive enhancement recommended: baseline styles without @scope, isolation as an enhancement.
Mironsoft
Modern CSS, Hyva themes and Magento frontend development
A CSS architecture that stays maintainable?
We analyze existing stylesheets, identify global style conflicts and modernize the CSS architecture with @scope, custom properties and Tailwind CSS, for Magento/Hyva projects that stay maintainable two years from now.
CSS audit
Specificity analysis, global conflicts and refactoring recommendations for existing stylesheets
Component styles
@scope implementation for Hyva components: clean, isolated and maintainable
Theme development
Custom Hyva themes with modern CSS, Tailwind v4 and Alpine.js