This is a written adaptation of my talk at CSS Day 2025. It was a lovely event, but I realize life is complicated and not everyone can make it to events like this. There are videos up paywalled at conffab.com. I figure this written version can make my points as well.
If it’s helpful to have the slides, here ya go:


I hate to break it to ya, but CSS is… mostly scope. The act of writing CSS is to:
- Write some scope
- Write some styles

Even something like .cool
is pretty extreme scoping. It only effects elements in the DOM that happen to have that exact class. OoooOoo rare.

It’s easy to think of CSS skill of being about knowing how to accomplish all the fancy styling bits. But the best CSS developers are also talented at knowing where and how to apply those styles. Scope.

Selectors are a big part of scoping, but there are other things in CSS that apply scope like @media
queries, @support
, and other at-rules.

Say we need to style a real-world design like this.

This part we could easily call a .card
and apply reusable styles accordingly.

Later, a new element comes up that is fairly card-like. Should we re-use the class? We’re at a mental CSS crossroads. The padding is a bit different, the colors are a bit different, there is no border-radius…

… meh. Screw it. Let’s call this one .challenge-card
.
We’ve accomplished scoping with our little ol’ fingers and brain.

99% of all the CSS scoping I’ve done in my life has been just like the above. Just use a different name and selector in order to avoid unwanted style collisions. Done.

There are problems that can come up here though. And there are tools to help with those problems.
But let’s reach for tools when we actually have the problem, not because the theoretical problem exists.

Regular ol CSS is “global” in nature.
Maybe your brain keeps thinking of using .card
as a class name, only to discover it’s already being used. Worse, you write up your styles using that name, it all looks great, and you don’t realize you’ve steamrolled other parts of the site with your new clashing styles. Derp.

Two different developers at different times can choose the same names and selectors and cause issues, possibly without even knowing it. That’s probably the worst and most insidious problem.

We could call that “The Barstool Problem”.
That is, writing CSS that affects styles elsewhere that you didn’t intend.

If you face problems like this, which I’d say is a reasonable concern as soon as 2 or more people are working on a project, or the project is of Medium™ size or bigger, it may be time to tool the problem away.

There is stuff in the web platform to help… sort of. We’ll get to that. But let’s do userland tools first.

One approach to scoping styles is to process the HTML/CSS/JavaScript that we write such that the selectors being applied are unique and won’t cause The Barstool Problem.

CSS Modules is interesting in that this kind of scoping is the only thing that it does. It’s whole reason for existence is scoping class names.
It’s also 10 years old just recently, congrats CSS Modules!
I love how it was worked on in 2015 and that’s basically it. Feature complete for 10 years.

Here’s how it works.
You write regular CSS (nice!).
The CSS that ends up on the page is that class plus some gibberish, which accomplishes the scoping part.

How does the HTML know to match to that class? You import the styles in your JavaScript. What you get is an object that maps the class names you wrote to the gibberish ones that will match.
That’s it really. So CSS Modules is only relevant in JavaScript-produced markup.

No barstools will be kicked over in this situation. If 50 developers all use .card
as a class name, they won’t clash.
In practice, giving the top-most element of the component a class name of root
is nice because standard conventions mean you don’t have to think too hard about it.

I also enjoy how CSS Modules is just an idea. Tons of bundler-type applications support it, but they do so by just following the spec of how it should work.

Out in “the real world”, here’s a React component that imports a CSS files that uses CSS modules conventions (which also use Sass just for fun, it’s being processed anyway). The className
is applied.
Crucially, this co-locates the styles and the component.

When you scope and co-locate styles, it really lessens the issue of “unused CSS”. All-global CSS tends to grow over time and lead to CSS files that developers are (rightfully) afraid to touch (barstools) and that are essentially wasted bandwidth.
Scoped, co-located CSS means it’s less likely there is unused CSS in those files. If the component isn’t used, the CSS doesn’t load at all (typically).

Junk the component, junk the styles along with it. No more orphaned styles.

This style co-location happens in the concept of CSS-in-JS as well. Usually not my favorite approach to CSS, but I do like the idea of keeping the styling job close to the components themselves.

I do not think the world is better off because React is unopinionated about styles. We would have been better off if they just had a blessed styling technique.

Vue has a blessed styling technique which does scoping and it’s perfectly nice.

I like how the [data-attribute]
it adds means you get to keep the exact class you authored too, which won’t change over time, so you’re maintaining nice hooks for people’s user stylesheets.

Svelte also has a blessed styling technique which scopes and it’s fine.

Can the web itself help us with selector scoping? Maybe not exactly like this, but we’ll see. The scoping it traditionally provides is either super loose (just use a different class) or super hardcore.


Websites can include other… websites. By way of the venerable <iframe>
, of course. They are fraught with challenges, but they were foundational to the last three companies I worked at, so respect.

Also known as iFrames
, of course.

Designed by Apple in California.

For real though, you can show web content with them, and there’s no way CSS from the outside will leak in or CSS from inside leak out. But they are a brick wall and not a practical choice just for this purpose alone.

Shadow DOM is also pretty hardcore scoping. Styles that inherit can sneak through a shadow DOM boundary (including custom properties), but little else. This hardcore scoping is largely why it exists.

The first time I came across the shadow DOM was when I advocated for <use>
in SVG as a better alternative for icons than using icon fonts. When you <use>
another bit of SVG, it clones it into a shadow root and becomes an <svg>
of it’s own.
Form elements often use shadow roots to abstract away their UI implementations as well.

We can make our own shadow DOM now with Web Components. But bear in mind you don’t have to. Keeping to the light DOM can be awfully nice.

As we’ve covered, scoping can be kinda great, and the fact that we can opt-in to it in Web Components is nice. It’s a one-liner to make a custom element have a shadow DOM.
Here, I’m injecting a <style>
tag into that shadow DOM, which will inherently be scoped.

The selector used in that style tag is simply: button
. And notice that only the <button>
that is within the Web Component is styled with it, not the <button>
outside of it. Sometimes that’s exactly the goal.

Imagine not just a button but an entire system of components that only look the way they do because of baked-in scoped styles on themselves. No worries of their styles affecting other elements, or, for the most part, other styles coming in and screwing things up.
Sounds pretty good for design systems right?

That’s not just an “oh yeah, hmmm, maybe that could work” situation. Pretty much all the major design systems ship with shadow DOM scoping.


When we talk about styling via classes, there used to be several different names of projects we could point to, but these days let’s be real: it’s Tailwind.

Since you aren’t writing classes or other selectors to do styling in Tailwind, the styles you are applying are already scoped.
Even if it’s not the main reason people choose a tool like Tailwind, it is a potential benefit.

Even if I admit the approach isn’t for me, Tailwind does have some objectively good characteristics. One being that the CSS output is generally a good bit smaller than “normal” because of the de-duplication. Perhaps the HTML is a bit bigger, but it’s CSS that is more of a blocking resource, so it’s probably a net-win.
I mostly don’t like it because it feels like you need to know CSS anyway, and now you’ve got to use this leaky abstraction on top of it, and when I’ve tried it I feel like I’m not buying much.


CSS actually has a thing called @scope
now. Props to Miriam Suzanne for brainchilding and shepherding it.
My knowledge of @scope
is just from playing around and reading the work of people who write on MDN and for Google’s sites and fellow bloggers. So thank you to everyone who makes technical content. I probably read it.

If you’re wondering about browser support, basically the story is it’s waiting for Firefox, which already has it under a flag. It’s in Interop 2025, so it shouldn’t be too long.
I’d say it doesn’t progressively enhance terribly well, so chill on it a sec.

The first glance at the syntax had me feeling it left, uh, something to be desired.

Here’s some perfectly reasonable HTML and some perfectly reasonable CSS to go with it.

Rather than writing .card {}
we could write @scope (.card) {}
and it’s… almost exactly the same functionality just more characters to type.
At least, that was my initial impression.

For the record, we should be able to test the support of it like this. But we can’t. Because at-rule()
, while agreed upon, isn’t implemented anywhere yet. When it is, it’s likely any browser that supports it will also support @scope
so this will probably never be a useful combo.

While that very basic demo of the syntax isn’t dripping with usefulness, there are some somewhat niche things that @scope
can do that do have their uses.


Nicole Sullivan blogged scope donuts in 2011 and it’s just now a thing. So that’s a lesson for you. If you want something in the web platform, make a good case for it then wait 15 years.
Donut scoping is a way to select an element and allow descendent selectors as usual, up until a point you specify where descendent selectors no longer apply.

I always think of element that contain body copy as a use case. Like chunks of converted markdown are special little bubbles with their own styles and you want to prevent other styles leaking int there that you don’t want.
Here’s an example of an article on WIRED where we can clearly see some body copy (and non-body copy).

Links within body copy should be underlined. And they do that. Good job WIRED.

It’s arguable, but links in some other places may not need to be underlined as there is enough visual affordance that they are links anyway, like in navigation or article cards.

A common way to handle links-in-some-areas vs not-in-others is to let links be underlined by default, then select areas to remove them and remove them.

The reverse of that is to remove link underlines everywhere, and re-apply them places you want them, like in body copy content.

Donut scoping gives us a rather elegant way to express this idea. Remove links everywhere except within element with a class name of content
. That reads decently well to me.

Donuts can have more than one hole, depending on what you’re doing in the DOM.

Massive upheaval in how we write CSS? No. But it’s a nice little niche tool and CSS is better for having it.


Proximity scope is rather amazing in that it’s an entirely new “level” by which the browser decides which CSS rules to apply. We’ll see that in a moment as we look at a basic use case.

I wouldn’t say it’s ultra common, but one way to implement color themes is to use class names on wrapper elements that signify which theme is to be used there.

Say we have a dark and a light theme, those themes have more jobs to do that set one background-color
and one color
. It might change lots of stuff.
Think about link colors. Blue links may need to be lighter on a dark color and darker on a light color. So we’re expressing that here with an oklch()
color that adjusts the lightness.

Themes could be nested. Again maybe not ultra-common, but if they are just classes, it could be done. Imagine an entire page in light mode, but the footer swaps to dark mode as it’s a nice look. That might end up nested if we’re talking like…
<body class="theme-light">
...
<footer class="theme-dark">
...
Code language: HTML, XML (xml)

If we define our themes as single classes one after another, we might run into problems. Each theme will have an indentical specificity, so the last one declared will be the “most powerful” because of source order.
See the links now. Those links are technically within an element with theme-light
and theme-dark
. But theme-light
is “more powerful” as it’s declared last, and thus we get the wrong color.

Scope can help here. By replacing those theme selectors with like @scope (.theme-dark) {}
now proximity scoping kicks in.

Proximity is less powerful than specificity of selectors, but it’s more powerful than source order. That’s a little weird to wrap your mind around at first, I think. But this simple demo hopefully helps.

Because the theme-light
class is “closer” in the DOM to those links, it will “win”. Source order doesn’t matter here anymore, it matters which of the otherwise-equal selectors has higher proximity.

Big sea change in CSS? Again, no, but CSS is better off for having this niche tool.

Anytime source order might be a concern, it’s possibly proximity styling can step in to help.

Imagine variation classes on a “card” element. If the variation classes have the same specificity as the base class, the worry is the base class will override what the variation is doing.
This could be the case in systems that bundle CSS in ways you don’t fully understand, or load CSS on demand in ways that even user actions may affect the order.



See this bit of HTML. Two <div>
s, one of them with a <style>
tag inside and one without.
Inside that <style>
tag we immediately see an @scope
at-rule, with no parens at all. What is the scope then? Used like this, the scope is the parent element of the <style>
, so the <div>
parent. From there, we can select that <div>
or any descendants.

All the sudden we have a way to style from just one particular element downwards without even having to name it order use any selector at all.

I said the web platform doesn’t really have selector scoping help, but this is basically that. This is probably as close as we’ll ever get.
It feels like a pretty powerful concept to me.

What if we just did this “instead” of linking up CSS files and finding scoping solutions there? Maybe this is just how we style most everything on the page. Perhaps from the “component” level on down. Is that a reasonable thing to do?

I did attempt to build a demo with 1,000 cards where one page loaded CSS “regularly” with one stylesheet that applies to all the cards. Then another page where each of the 1,000 cards has a @scope
-d <style>
block within it.

You’d think the 1,000 extra <style>
blocks would be awful for performance, but, quite weirdly, the difference was almost undetectable. I think this was a poor test though as there was so much of the exact same code the browser was probably great at optimizing it. A better test would be tons of totally different components and include interactivity.

At a minimum, sprinkling in scoped styles into the DOM where you just want a bit of styles that will never “leak out” and screw up anything else is super cool.

Cheers!
This really reframes how I think about writing CSS—scope isn’t just about avoiding conflicts, it’s about intentional design decisions. I appreciated the example of deciding whether to reuse a class or create a new one like
.challenge-card
; it’s such a common dilemma and shows how much CSS is about thoughtful architecture, not just styling.I don’t think this is a fair assessment—as if there are no downsides. Not all user agents are rendering that CSS… headless clients, TUI browsers, cURL, & so on. Miles of CSS classes are clogging up their pipes to no benefit. The detriment is that many are completely skipping any basic semantics to their code to where you can’t select on anything for scraping, userStyles/userScripts, or any other personal need.