@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
@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:
@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:
@scope (.light-theme) {
p { color: black; }
}
@scope (.dark-theme) {
p { color: white; }
}<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:
@scope (.feature) {
:scope {
background: rebeccapurple;
padding: 2rem;
}
h2 {
color: white;
}
}Inline scoping
You can scope styles right where they’re used:
<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!)