@scope just landed in Firefox 146. Real style scoping, no Shadow DOM or BEM naming conventions required.

The problem it solves

CSS has always been global. You write .card { padding: 1rem } and every card everywhere gets that padding. The workarounds have been:

  • BEM naming (.article-card__title--featured)
  • CSS Modules (build-time class name hashing)
  • Shadow DOM (full encapsulation, but heavy)

@scope gives us a native way to say “these styles only apply within this subtree.”

Basic syntax

css
@scope (.card) {
  h2 {
    font-size: 1.5rem;
  }

  p {
    color: gray;
  }
}

These styles only affect h2 and p elements inside .card.

The “donut scope” pattern

You can set both upper AND lower bounds:

css
@scope (.article) to (figure) {
  img {
    border: 2px solid black;
  }
}

This styles all images inside .article… except those inside figure elements. It’s called “donut scope” because you’re selecting a ring around the excluded center.

Scope proximity

When scopes overlap, the closest scope wins:

css
@scope (.light-theme) {
  p { color: black; }
}

@scope (.dark-theme) {
  p { color: white; }
}
html
<div class="light-theme">
  <p>Black text</p>
  <div class="dark-theme">
    <p>White text</p>
    <div class="light-theme">
      <p>Black text (closer to .light-theme)</p>
    </div>
  </div>
</div>

No more fighting with specificity to get nested themes right.

The :scope selector

Inside a @scope block, :scope targets the root element itself:

css
@scope (.feature) {
  :scope {
    background: rebeccapurple;
    padding: 2rem;
  }

  h2 {
    color: white;
  }
}

Inline scoping

You can scope styles right where they’re used:

html
<div class="widget">
  <style>
    @scope {
      p { color: blue; }
    }
  </style>
  <p>This is blue</p>
</div>
<p>This is not</p>

When you omit the scope root in a <style> tag, it automatically scopes to the parent element.

Browser support

As of December 2025:

  • Chrome/Edge: 118+
  • Safari: 17.4+
  • Firefox: 146+ (just shipped!)