Scoped CSS is back

avatar

Wang Daye

 Fujian

Read 4 minutes

First published on the official account of  

Great Move to the World , welcome to pay attention. 📝 7 practical front-end articles every week 🛠️ Share development tools worth paying attention to 😜 Share interesting things in the process of personal entrepreneurship

Come and experience the ChatGpt plus version for free. The trial address for our money is: https://chat.waixingyun.cn. You can join the technical group at the bottom of the website to find bugs together. In addition, the new version of the drawing artifact has been launched https://cube.waixingyun .cn/home

Scoped CSS disappeared a few years ago, and now it’s back and much better than its previous version.

Even better, the W3C specification is largely stable and there is now a working prototype in Chrome. We just need the community to pay a little attention, entice other browsers to build their implementations, and get the job done.

What is this idea?

Scopes bring two key points to CSS:

  1. Better control over which selectors target which elements (i.e. better manipulation of cascades).
  2. One set of styles can override another set of styles based on position in the DOM.

Local styles allow you to include a set of styles within a single component on the page. You can use .titlea selector, which only works inside the Card component, and use another .titleselector, which only works inside the Accordion. You can prevent a component’s selectors from targeting elements in child components, or allow them to reach them if desired.

You no longer need BEM-style class names.

Furthermore, proximity becomes a first-class citizen in the cascade. If two components target the same element (with the same specificity), the inner component’s styles will override the outer component’s styles.

How does it work?

Everything starts with @scoperules and a selector, like this:

@scope (.card) {
   /* Limit the following styles inside `.card`*/ 
  :scope {
     padding : 1rem ;
     background-color : white;
  }

  .title {
     font-size : 1.2rem ;
     font-family : Georgia, serif;
  }
}

These styles are restricted to .cardelements. :scopeIs a special pseudo-class for .cardthe element itself, .titlefor the title inside the title.

@scopeThe rules themselves do not add specificity to these selectors, so they are all ( 0, 1, 0). Yes, specificity still matters, but that’s a good thing™️. Talk later.

At this point, you can use a normal descendant selector to achieve this. But when you apply internal boundaries within a range or overlap multiple ranges on the page, new, previously impossible options start to appear. Let’s see how they do…

internal range bounds

Let’s say you expect to put other components into yours Cards, so you don’t want .titlethe selector to target anything other than the one that belongs to Card. To do this, you set an inner bound on the range, like this:

@scope (.card) to (.slot) {
   /* The restricted style is only inside `.card`, but not inside `.slot`*/ 
  :scope {
     padding : 1rem ;
     background-color : white;
  }

  .title {
     font-size : 1.2rem ;
     font-family : Georgia, serif;
  }
}

Think of the to keyword here as until : the range is defined from .cardto . .slotNow, there is no restricted selector that targets .slotanything inside the Card element. So you can build your card like this:

< div  class = "card" > 
  < h3  class = "title" > Moon lander </ h3 > 
  < div  class = "slot" > 
    <!-- Partial styles will not target anything here! --> 
  </ div > 
</ div >

The scope of the effect is limited so that it does not target .slotanything within it. This way you can nest two scopes, each using the same common header class name, without conflict. In fact, you may no longer need the class name at all:

@scope (.card) to (.slot) {
   h3 {
     font-size : 1.2rem ;
     font-family : Georgia, serif;
  }
}

@scope (.accordion) to (.slot) {
   h3 {
     font-family : Helvetica, sans-serif;
     text-transform : uppercase;
     letter-spacing : 0.01em ;
  }
}

You can put an Accordion inside a Card, or a Card inside an Accordion, and their respective styles will not conflict.

This is colloquially known as a donut scope because there is a hole in the scope. (The inner border selector can also have multiple holes if it targets multiple elements.)

Miriam Suzanne suggests this approach by consistently using data-*attributes and attribute selectors as your scope:

@scope ([ data -scope= 'media' ]) to (:scope [ data -scope]) {
   /* The restricted style goes here */
}

Proximity precedence

Another aspect is the concept of proximity: styles from the inner scope will override styles from the outer scope. Imagine you have two scopes like this:

@scope (.green) {
   p {
     color : green;
  }
}

@scope (.blue) {
   p {
     color : blue;
  }
}

Apply the following to HTML. There are no internal scope constraints here, so both pselectors target the internal paragraph here. In this case, the inner scope always takes precedence:

<div class =" green ">
  <p> I am green </p>
  < div  class =" blue ">
    <p> I am blue </p>
  </div>​​
</div>​​

< div  class =" blue ">
  <p> I am blue </p>
  < div  class =" green ">
    <p> But I am green </p>
  </div>​​
</div>​​

Note this currently only works in Chrome and requires chrome://flagsthe Experimental Web Platform Features flag to be enabled in Chrome.

You can inspect it in DevTools and see how each range overrides the other based on its closest proximity:

The problem here is that selector specificity still takes precedence, so if the outer scope targets an element with higher specificity than the inner one, the styles from the outer scope will be applied.

This way, when two scopes target the same element, you can control which one takes precedence. Rather than always letting the inner scope win, you can adjust the selector’s specificity so that the higher-specificity selector takes precedence, regardless of which scope it belongs to.

When you don’t want this behavior, you have several ways to prevent it. You can use cascading layers to prioritize one component—or parts of a component—over another. Alternatively, you can apply an inner scope constraint to the outer scope to prevent it from happening. After trying the range for a while I feel like this is the right balance. It gives you maximum control rather than subjecting you to a cascading set of strict rules.

This is a game turning point

If you develop a large application and have to rely on CSS-in-JS libraries to prevent class name conflicts, scoped CSS is a good choice. If you use a complex BEM class name system and struggle to keep all selector specificities consistent, think of the freedom this can bring. If you’ve used shadow DOM to isolate styles but felt it was too heavy-handed, here’s a better approach (of course, shadow DOM still has its uses).

Here are just a few ideas I would try:

  1. Sections that define a component have an internal border, sections do not, so its “chrome” styling (i.e. wrappers, toggle buttons, etc.) does not affect its child content, but it can affect the appearance of the text within it.
  2. Define parts of a component on different cascading levels so that it can affect the scope of its inclusion but is still easily overridable at higher levels.
  3. Nested color themes.
  4. Prevent style conflicts more easily in blog posts.
  5. Container queries—what can we come up with by mixing and matching?

We need more browser support

So far, Chrome seems to have support — they’ve had the first working prototype for a few months. It may be slightly behind the latest changes to the spec, so if you play around, keep an eye out for some minor changes coming.