Scope in CSS

Chris Coyier Chris Coyier on

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:

Chris Coyier presenting on stage at a conference, with a large audience seated in front of them. A screen displays coding content related to CSS.
A graphic titled 'How to CSS' featuring two steps: '1. Write some scope' and '2. Write some styles.' The background is black, with white and blue text, accompanied by a cartoon character pointing and wearing a yellow 'For Dummies' shirt.

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

  1. Write some scope
  2. 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.

Image displaying the 'CSS Modules' logo, celebrating its 10-year birthday, accompanied by a screenshot of GitHub insights showing contributions and commits over time.

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.

Diagram showing the import of styles in a JavaScript component, highlighting the transformation from CSS class names to a unique identifier for scoped styling.

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.

A stool placed beside a computer screen displaying produced HTML and CSS code for a card element.

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.

A graphic featuring the text 'MILD CSS in React' with a chili pepper icon, alongside a list of CSS frameworks and libraries such as Styled Components, Emotion, and CSS Modules.

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.

Text graphic featuring the words 'Hardcore SCOPING' with lightning effects against a dark background.

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.

A smiling man wearing glasses and a black turtleneck (Steve Jobs) holds a small white rectangular object, with the text '<iFrame>' overlaid on the image.

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.

Text on a white background discussing Web Components and Shadow DOM, emphasizing that Web Components can create their own Shadow DOM but do not have to.

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.

Code snippet demonstrating a custom HTML element 'FancyButton' that uses Shadow DOM to encapsulate styles.

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.

An example of a web component button coded in HTML and JavaScript, featuring a styled button labeled 'Web Component Button' within a custom element called 'fancy-button', alongside a regular button labeled 'Not Web Component Button'.

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.

Text slide discussing the effectiveness of Web Components for design systems.

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.

Image featuring the text 'UTILITY CLASS SCOPING' in bold black font, with a green pixelated font for 'UTILITY CLASS', and a subtitle 'Styling via classes that do one thing each.' in a standard font.

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.

A graphic featuring the text 'Donut SCOPE' in large, playful typography with a pink background and colorful accents. The subtitle reads 'The "Stop Caring Now" Selector.'
Screenshot of a web page titled 'Scope donuts' by Nicole Sullivan, with annotations highlighting the word 'GOAT' and the year '2011' in a circle, showing a discussion about HTML5 and styling.

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.

A cartoon character resembling a man with a bald head, holding a large pink donut with the word 'Donut' in a playful font above it, accompanied by the phrase 'lexical scoping' written in cursive below.

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

Graphic depicting the word 'Proximity' in bold, black font on a bright yellow background, accompanied by an abstract circular design.

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.

A flowchart illustrating CSS hierarchy concepts, including origin and importance, context, element attached styles, layers, specificity, scope proximity, and order of appearance.

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.

A graphic featuring the text 'DOM Blasters' in a stylized font with pixel art space invaders, set against a starry background. Below, the tagline reads 'One-offs FTW'.

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?

A grid layout displaying multiple 'Premium Experience' cards, each featuring checkmarks and descriptions of various features, with a prominent 'Get Started' button.

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.

A screenshot of a blog post editor on the left and an example of the published blog post on the right. The editor displays HTML and CSS code with a scoped style rule, while the published post features a large title in a stylish font and a subtitle in red.

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.

Slide with a blue background and yellow text titled 'The Takeaways'. 

• CSS is naturally scoped. That might be all you need.

• Selector scoping is pretty sweet on large sites with teams. Lots of existing tools help with selector scoping, or avoid the need for it.

• Native CSS @scope is nice to have. Donus scoping and proximity are a small upgrade to CSS.

• <style> block @scope might be great. Tool-free component scoping. Can we make it performant?

Cheers!

Wanna learn CSS from a course?

Frontend Masters logo

FYI, we have a full CSS learning path with multiple courses depending on how you want to approach it.

7-Day Free Trial

2 responses to “Scope in CSS”

  1. 4o Image API says:

    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.

  2. toastal says:

    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 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.

Leave a Reply

Your email address will not be published. Required fields are marked *

Did you know?

Frontend Masters Donates to open source projects. $363,806 contributed to date.