← Back to Blog
Deep Dives·Jan 26, 2026·6 min read

CSS @scope Is Finally Baseline — Time to Rethink CSS-in-JS?

Native style scoping landed in all browsers. Here's what it means for React developers who've been shipping runtime CSS.

CSSReactPerformanceNext.js
JV

Jose Viscasillas

January 26, 2026 · 6 min read

I've been writing React for almost a decade now. In that time, I've watched the community cycle through every CSS solution imaginable: plain CSS with BEM conventions, CSS Modules, styled-components, Emotion, Tailwind, and whatever new hotness drops each year. The core problem was always the same: CSS is global by default, and that's terrifying in component-based architectures.

Well, as of January 2026, CSS @scope is officially baseline. Every major browser supports it. And honestly? It might be time to reconsider whether we still need all that CSS-in-JS machinery.

The Problem We've Been Solving Wrong

Here's the thing that always bothered me about CSS-in-JS solutions: we invented entire runtime systems to solve a cascade problem. styled-components generates unique class names at runtime. Emotion does the same. We're shipping JavaScript to generate CSS to avoid... CSS doing what CSS does.

That's not elegant engineering. That's a workaround.

jsx
// This works, but at what cost?
const Card = styled.div`
  padding: 1rem;
  background: white;
  
  h2 {
    color: blue; // Won't leak... because we generate unique classes at runtime
  }
`;

The runtime overhead is real. The bundle size is real. The hydration mismatch potential is real. We've just collectively agreed it's worth it because the alternative — global CSS chaos — was worse.

Enter @scope

CSS @scope lets you define boundaries for your styles. Styles inside a scope only apply within that scope. No JavaScript. No build step magic. Just CSS doing what CSS should have done all along.

css
@scope (.card) {
  :scope {
    padding: 1rem;
    background: white;
  }
  
  h2 {
    color: blue; /* Only affects h2 inside .card */
  }
  
  p {
    line-height: 1.6; /* Only affects p inside .card */
  }
}

That h2 style won't leak to any other h2 on the page. Period. The browser handles it natively, with zero runtime cost.

The Donut Scope Pattern

Here's where it gets interesting. You can also define a limit to your scope — creating what's called a "donut scope." The styles apply from the scope root down to (but not including) the limit.

css
@scope (.card) to (.card-footer) {
  p {
    color: #333;
  }
}

This means paragraphs inside .card get the styles, but paragraphs inside .card-footer don't. The footer is the "hole" in the donut.

Why does this matter? Because components nest. Cards contain cards. Modals contain cards. Without scope limits, you'd get the same cascading chaos we've been fighting for years.

html
<div class="card">
  <p>This gets the scoped styles</p>
  <div class="nested-card">
    <p>This could inherit unwanted styles...</p>
  </div>
</div>

With donut scoping:

css
@scope (.card) to (.nested-card) {
  p {
    font-size: 1rem;
  }
}

The nested card's paragraphs are excluded. Clean boundaries. No specificity wars.

Using @scope in Practice

There are two ways to use @scope:

1. In External CSS Files

css
/* components.css */
@scope (.navigation) {
  :scope {
    display: flex;
    gap: 1rem;
  }
  
  a {
    color: inherit;
    text-decoration: none;
    
    &:hover {
      text-decoration: underline;
    }
  }
}

@scope (.hero) {
  :scope {
    min-height: 80vh;
    display: grid;
    place-items: center;
  }
  
  h1 {
    font-size: clamp(2rem, 5vw, 4rem);
  }
}

2. Inline with Style Tags

This is where it gets really cool for component-based thinking:

html
<article class="post">
  <style>
    @scope {
      :scope {
        max-width: 65ch;
        margin: 0 auto;
      }
      
      h2 {
        margin-top: 2rem;
      }
      
      code {
        background: #f5f5f5;
        padding: 0.2em 0.4em;
        border-radius: 3px;
      }
    }
  </style>
  
  <h2>Article Title</h2>
  <p>Content with <code>inline code</code>...</p>
</article>

When you use @scope without a selector inside a <style> tag, it automatically scopes to the parent element. That's genuinely useful for one-off components where you don't want to pollute a global stylesheet.

What This Means for React Developers

Let's be real: you're probably not ditching styled-components tomorrow. Your codebase is what it is, and migration is expensive. But for new projects? New components? This changes the calculus.

Consider CSS Modules + @scope

CSS Modules already give you local scoping via build-time class name generation. Adding @scope on top gives you even more control:

css
/* Card.module.css */
.card {
  /* Base styles */
}

@scope (.card) to ([data-slot="actions"]) {
  button {
    /* Styles for buttons in the card body, but not in actions slot */
  }
}

Consider Vanilla CSS in Next.js App Router

Next.js App Router colocates components by default. You can drop a .css file next to your component and import it:

text
app/
  components/
    Card/
      Card.tsx
      Card.css  ← scoped styles here
css
/* Card.css */
@scope (.card) {
  :scope {
    border: 1px solid #e5e5e5;
    border-radius: 8px;
    padding: 1.5rem;
  }
  
  header {
    border-bottom: 1px solid #e5e5e5;
    padding-bottom: 1rem;
    margin-bottom: 1rem;
  }
}

No runtime. No JavaScript. Just CSS that works.

The Performance Argument

I ran some quick benchmarks (unscientific, but directionally correct) comparing a page with 100 styled-components vs. the same styles using @scope:

ApproachJS Bundle ImpactFirst PaintHydration Time
styled-components+45KB1.2s180ms
CSS @scope0KB0.9s40ms

The difference is more pronounced on slower devices. CSS-in-JS has to parse JavaScript, generate styles, inject them into the DOM, and then React has to hydrate the components. Native CSS just... works.

When to Still Use CSS-in-JS

I'm not saying burn it all down. CSS-in-JS still wins when you need:

  1. Dynamic styles based on propscolor: ${props => props.variant === 'primary' ? 'blue' : 'gray'} is hard to replicate
  2. Theme injection at runtime — though CSS custom properties handle most cases now
  3. Automatic critical CSS extraction — some CSS-in-JS libs are genuinely good at this
  4. Team familiarity — the best tool is often the one your team knows

But if you're reaching for styled-components purely to avoid style leaking? That's exactly what @scope solves, without the overhead.

Browser Support Reality Check

As of January 2026, @scope is baseline:

  • Chrome 118+ ✅
  • Safari 17.4+ ✅
  • Firefox 146+ ✅ (shipped December 2025)
  • Edge 118+ ✅

If you're supporting older browsers, you'll need a fallback strategy. The good news: @scope degrades gracefully. Unsupported browsers just ignore the at-rule and apply the styles globally — which is what you had before anyway.

css
/* Fallback pattern */
.card h2 {
  color: blue; /* Always works */
}

@scope (.card) {
  h2 {
    color: blue; /* Takes over in supporting browsers */
  }
}

My Take

After 21 years of writing CSS, I've seen a lot of "this changes everything" features that didn't. But @scope feels different. It solves a real problem — style leakage in component architectures — at the platform level.

Will it replace CSS-in-JS entirely? Probably not. The ecosystem is too entrenched. But for new projects, especially ones where performance matters (so... all of them?), native scoping is compelling.

I've already started using it on this site. The mental model is cleaner: components own their styles, styles don't leak, no JavaScript required. That's the promise CSS-in-JS made, delivered by CSS itself.

Give it a shot on your next feature. You might be surprised how little you miss the runtime.

JV

Written by Jose Viscasillas

Senior Software Engineer building video platforms at ON24. 21 years of coding experience. I write about React, TypeScript, AI, and developer tools.

Recommended Reads

📬

Subscribe to the Newsletter

New articles delivered to your inbox. No spam, unsubscribe anytime.

Join 500+ developers getting weekly insights on React, TypeScript, and building products.