CSS@scope

@scope

@scope limits which elements a block of style rules applies to, using explicit start and end boundaries rather than relying purely on selector specificity or naming conventions. It gives plain CSS a form of the style isolation that CSS Modules or CSS-in-JS libraries have long provided at build time — but resolved natively by the browser, with no build step.
Basic scoping

CSS
/* These rules only apply inside .card */
@scope (.card) {
  :scope {
    border: 1px solid #ddd;
    border-radius: 8px;
    padding: 1rem;
  }

  h3 {
    margin-top: 0;
    color: #0066cc;
  }
}
:scope inside the block refers to the scoping root itself — the element matched by .card — the same way & refers to the parent in native nested CSS.
Donut scoping — excluding a nested boundary
The genuinely new capability is the optional to clause, which defines a lower boundary the scope should NOT cross. This produces a "donut" shape: the styles apply within the outer boundary, but stop being applied once they reach the inner boundary, even though elements past that boundary are still physically nested inside the outer one in the DOM.

CSS
/* Styles apply within .card, but NOT inside .card-content —
   even though .card-content lives inside .card in the DOM */
@scope (.card) to (.card-content) {
  p {
    color: #333;
    font-size: 0.95rem;
  }
}

HTML
<div class="card">
  <p>This paragraph IS styled — it's inside .card, before the boundary.</p>

  <div class="card-content">
    <!-- Anything rendered here (e.g. arbitrary user/CMS content, or a
         nested unrelated component) is OUTSIDE the donut, so the
         .card scope's <p> rule does NOT reach this paragraph. -->
    <p>This paragraph is NOT styled by the .card scope.</p>
  </div>
</div>
Use case: component-like isolation without a build step
The donut model is particularly useful for component patterns where a wrapper renders arbitrary nested content it doesn't fully control — a card that can contain a rich-text body, a comment widget, or a third-party embed. Scoping the card's own styles to stop at the content boundary avoids accidentally reaching into that nested content with selectors like a bare p or h3, which would otherwise cascade all the way down.

CSS
@scope (.comment-thread) to (.comment-body) {
  /* Styles the thread's own chrome — avatars, timestamps, layout —
     without leaking into user-generated comment text */
  :scope {
    display: flex;
    gap: 0.75rem;
  }

  .avatar {
    width: 40px;
    border-radius: 50%;
  }

  a {
    color: #0066cc;
    text-decoration: none;
  }
}
Proximity — scoped rules can beat specificity
When two scoped rules from different @scope blocks could both match the same element, the one whose scoping root is closer to the element wins, independent of normal specificity — which is a useful, predictable tie-breaker for nested component styles that would otherwise need extra classes to disambiguate.
  • @scope can be used without a to clause for simple "apply only within this root" scoping.

  • The scoping root selector (.card) can be any selector, not just a class — including combinators.

  • :scope inside the block is optional; unqualified selectors like h3 still only match within the scope's boundaries.

Note
Browser support for @scope is newer than most of the rest of this series — check current status before relying on it as your only isolation mechanism in production, and treat it as a genuinely useful addition for cases where reaching for full CSS Modules or a CSS-in-JS library would be overkill.
Next
Animate an element's very first appearance: [@starting-style (entry animations)](/css/starting-style).