CSS @scope Explained: Native Component Scoping and the Donut Pattern Image
To Blogs

CSS @scope Explained: Native Component Scoping and the Donut Pattern

TL;DR — Quick reference

Syntax
What it does
@scope (.card) { }
Styles only elements inside .card
@scope (.card) to (.bio) { }
Styles inside .card but stops at .bio (Donut)
:scope { }
Targets the scoping root element itself
<style>@scope { }</style>
Auto-scopes to the parent element. No class needed

@scope is now Baseline Available across all major browsers. For a decade, we’ve used workarounds to keep our CSS from leaking. BEM naming conventions (.card__title--large), CSS Modules that hash class names into gibberish, and the heavy-handed isolation of the Shadow DOM.

In 2026, the browser finally gives us a native, lightweight solution: @scope. It allows you to define exactly where a style starts and, more importantly, exactly where it stops ,with no build step, no extra JavaScript, and no framework required.

Browser Support

Before adopting any new CSS feature, you need to know where it runs. Here’s the current support picture:

Browser support

Chrome / Edge
v118
Oct 2023

Supported

Safari
v17.4
Mar 2024

Supported

Firefox
v128
Jul 2024

Supported

Safe to use in production today. No polyfill required for any modern browser.

If you need to support legacy environments, a progressive enhancement approach works well. Write your baseline styles first, then layer @scope on top.

The Problem: Global Style Leakage

CSS is global by nature. If you style a .title inside a .card, and then nest a .profile component inside that card which also has a .title, the card’s styles will “bleed” into the profile.

/* Intended for .card, but bleeds into .profile too */
.card .title {
  font-size: 2rem;
  color: navy;
}

We used to solve this with ever-increasing specificity or complex selectors. But @scope changes the game by creating a local boundary without the complexity of Web Components.

How @scope Works

The @scope rule limits selectors to a specific subtree of the DOM. It tells the browser: “Only apply these styles to elements that live inside this specific parent.”

Basic Scoping

/* Styles only apply to elements INSIDE .card */
@scope (.card) {
  .title {
    font-weight: 800;
    color: var(--primary);
  }

  img {
    border-radius: 12px;
  }
}

An img tag outside of .card remains completely untouched. No specificity wars, no accidental global overrides.

Inline Scoping with <style> Blocks

One of the most underrated features of @scope is that you can write it inside a <style> tag within your HTML, and it will automatically scope to the parent element . No class name required.

<article>
  <style>
    @scope {
      h2 { color: var(--primary); }
      p  { line-height: 1.7; }
    }
  </style>
  <h2>This is scoped</h2>
  <p>So is this.</p>
</article>

Any h2 or p outside this <article> is completely unaffected. This is a game-changer for component-based frameworks like Astro, Svelte, and Web Components, where co-locating styles with markup is the goal.

The “Donut” Pattern: Scoping with Holes

The most powerful feature of @scope isn’t just defining where styles start . It’s defining where they stop. This is Donut Scoping.

Visualising the Donut

Consider this DOM tree:

.card                      ← Scope root (styles START here)
├── h2.title               ✅ styled
├── p                      ✅ styled
└── .user-bio              ← Scope limit (styles STOP here)
    ├── h2.title           ❌ NOT styled (inside the "hole")
    └── p                  ❌ NOT styled (inside the "hole")

The .card is the doughnut. The .user-bio is the hole punched through the middle. Styles fill the doughnut but never enter the hole.

Creating the “Hole”

@scope (.card) to (.user-bio) {
  /* Applies to .card's content, but stops before .user-bio */
  p {
    color: var(--gray-600);
    line-height: 1.5;
  }

  h2 {
    font-size: 1.5rem;
    font-weight: 700;
  }
}
  • The Root (.card): Where the styles begin.
  • The Limit (.user-bio): Where the styles stop, this element and everything inside it is excluded.

This allows you to style a component’s shell while ensuring nested child components remain pristine and unaffected by the parent’s CSS.

Proximity Specificity: The Paradigm Shift

This is the most revolutionary (and most overlooked) aspect of @scope: when two scopes both match the same element, @scope resolves the conflict by proximity, not by specificity.

@scope (.card) {
  .title { color: blue; }
}

@scope (.card .featured) {
  .title { color: gold; }
}

In traditional CSS, this would be a specificity battle you’d solve by adding more selectors or reaching for !important. With @scope, the style from the closest ancestor scope wins automatically. The .featured scope is closer to .title in the DOM, so gold wins. Cleanly, predictably, and without a specificity arms race.

This is a fundamental shift in how CSS resolves conflicts, and it makes component-driven architectures significantly easier to maintain at scale.

The :scope Pseudo-Class Inside @scope

When you write :scope inside an @scope block, it refers to the scoping root element itself . Not the document root (which is what :scope means outside of @scope).

@scope (.dark-theme) {
  /* Targets .dark-theme itself */
  :scope {
    background: #111;
    color: #eee;
  }

  /* Targets <a> tags inside .dark-theme */
  a {
    color: var(--accent-cyan);
  }
}

This lets you style both the container and its children within a single @scope block, making it ideal for Theme Islands (see below).

Practical Use Case: Theme Islands

You can use @scope to create isolated “Theme Islands” within your app (sections with a completely different visual theme) without any risk of styles bleeding into adjacent components.

@scope (.dark-theme) {
  :scope {
    background: #111;
    color: #eee;
    border-radius: 16px;
    padding: 2rem;
  }

  a    { color: var(--accent-cyan); }
  h2   { color: #fff; }
  code { background: #333; }
}

Apply .dark-theme to any section and only that section flips. Your light-themed sidebar, header, and footer are completely unaffected. Guaranteed by the scope boundary, not by hoping no selectors conflict.

Why This Beats CSS Modules

CSS Modules are great, but they come with real trade-offs:

CSS Modules vs native @scope

CSS Modules
Native @scope
Build step required
Yes (Vite, Webpack…)
No
Readable class names in DevTools
No (.btn_x5j2l)
Yes
Co-located with markup
Separate file
Inline <style>
Reacts to DOM structure
No (file-based)
Yes
Framework-dependent
Often yes
Pure browser
Proximity-based resolution
No
Yes

CSS Modules still have a place in large React codebases with complex build pipelines, but for most modern projects (especially those using Astro, vanilla HTML, or any framework that supports <style> blocks) native @scope is a simpler, more powerful alternative.

Pro Tip: Low Specificity Wins

@scope does not increase specificity in the way a deeply nested selector would. A selector inside @scope is intentionally weaker and easier to override than something like .main-nav .menu-item .link-active.

This means:

  • No more !important to override a component style
  • No more specificity calculator before writing a selector
  • Styles do exactly what they say, nothing more

The weaker the selector, the more maintainable your codebase becomes as it grows.

When NOT to Use @scope

No tool is right for every job. Skip @scope if:

  • You rely heavily on Shadow DOM. Shadow DOM already provides hard encapsulation. Adding @scope on top is redundant.
  • Your team uses CSS-in-JS universally (Styled Components, Emotion). The scoping problem is already solved at the JS layer.
  • You need IE11 support. @scope has zero legacy browser support. Use BEM or CSS Modules for those environments.
  • The component is truly global. Things like reset styles, typography scales, and utility classes are meant to be global, wrapping them in @scope defeats the purpose.

Bonus: Copy-Paste Patterns for Tomorrow

Here are the concrete patterns you can put to use immediately:

1. Scope a UI component:

@scope (.card) {
  .title  { font-size: 1.25rem; }
  .body   { color: var(--gray-700); }
}

2. Donut-scope to protect a nested component:

@scope (.card) to (.user-bio) {
  p { line-height: 1.6; }
}

3. Auto-scope with an inline <style> block:

<section>
  <style>
    @scope { h2 { color: var(--brand); } }
  </style>
  <h2>Scoped automatically</h2>
</section>

4. Create a theme island:

@scope (.dark-section) {
  :scope { background: #0f0f0f; color: #fafafa; }
  a      { color: cyan; }
}

The cascade has always been a feature, not a bug. @scope finally gives you the precision to use it intentionally. You get CSS encapsulation, component isolation, and proximity-based conflict resolution, all natively in the browser with zero tooling overhead.