DEV Community

𒎏Wii 🏳️‍⚧️
𒎏Wii 🏳️‍⚧️

Posted on • Edited on • Originally published at blog.but.gay

What is CSS @scope and why should I care?

A brief history of Scoping and CSS

Scoping CSS to certain regions of the DOM is not a new idea.

The scope attribute for <style> tags was one attempt at addressing it. Nowadays, it is sadly deprecated though.

/* ⚠ This is deprecated. It doesn't and won't work ⚠ */
<div>
   <style scope>p { color: red; }</style>
   <p>Red Text</p>
</div>
<p>Normal Text</p>
Enter fullscreen mode Exit fullscreen mode

Many front-end frameworks implement their own scoping by prefixing CSS rules with IDs and classes and adding those to their HTML output. However, this requires a lot of complexity in the framework and is still brittle.

Then components came into the browser, in the form of custom elements and shadow-DOM. In fact, one part of shadow-DOM is that all the CSS inside it is scoped. However, it doesn't permit outside CSS to leak inside either.

Native Scoping is still on the table

The exact reason why scope was originally abandoned seems a bit fuzzy. Some will tell you it was because browsers didn't want to implement it, others say that it was just about letting web components become a thing, then re-evaluate the need for pure CSS scoping.

Whatever the case may be, CSS authors still seem to have an interest in scoping being a thing, for a variety of reasons.

CSS Scoping Revived: @scope

The @scope rule is the newest attempt at bringing CSS scoping to the browser. It is described in the Working Draft of the CSS Cascading and Inheritance Level 6 specification.

In other words: it's far from being usable. But there's still plenty of reasons to be hyped about it! 😁


The way this would work is simple: we would first define where we want our rules to apply. We can use any CSS selector here, but to avoid distractions, I will be using the outer-component and inner-component custom elements for the rest of this article.

@scope (outer-component) {
   p { color: red; }
}
Enter fullscreen mode Exit fullscreen mode

Any rules written inside this scope block will only apply inside an element described by the selector.

<p>This text is black</p>
<outer-component>
   <p>This text is red</p>
</outer-component>
Enter fullscreen mode Exit fullscreen mode

And we can also describe a lower boundary to this scope; another selector telling the browser that the scope should not apply to a certain sub-tree.

@scope (outer-component) to (inner-component) {
   p { color: red; }
}
Enter fullscreen mode Exit fullscreen mode
<outer-component>
   <p>This text is red</p>
   <inner-component>
      <p>This text is black</p>
   </inner-component>
</outer-component>
Enter fullscreen mode Exit fullscreen mode

Why should we be hyped about it?

The example with two nested custom elements already shows one possible use-case. Having styles apply only inside specific components without prefixing every selector with the component name is already useful.

But the addition of a lower boundary to prevent styles from leeking into nested components makes this incredibly useful, specialy in the modern front-end landscape is constantly moving away from monolithic structures and towards small, portable and interchangeable components.

If you're writing plain CSS for some vanilla JS components, you will be able to write CSS that's much more similar to what frameworks like svelte allow you to do: Write a bunch of rules and they will only apply inside the component. Imagine doing that with direct-child selectors 😵‍💫

here's an example of where this could be useful without any components or custom elements:

th { background: black; color: white; }
@scope (table.vertical) to (table) {
   th::after { content: ':' }
   th { all: initial; }
}
Enter fullscreen mode Exit fullscreen mode

Meanwhile, for authors of such component frameworks, native CSS scoping will vastly reduce the complexity they have to deal with as they will no longer have to automatically prefix selectors nor add IDs or classes to the elements they should apply to.

A somewhat simplified version of what such a framework could do:

component_css = `
    @scope ([data-component="${component.name}"]) to ([data-component]) {
        ${component.styles}
    }
`
Enter fullscreen mode Exit fullscreen mode

Even if you just use these frameworks, which already implement CSS scoping, there might be some benefits for you. Most importantly: since the scoping happens in the browser at runtime, frameworks don't need to know about your elements in order to style them, so there will be much less friction between frameworks and manually generated content. Inserting some HTML via .appendChild() would "just work".

// No code example for this one, because these problems only tend
// to surface once the project becomes a little bit more complex
// and several libraries trying to work together.
Enter fullscreen mode Exit fullscreen mode

So there you have it! Scoped CSS might soon be the hot new thing in latest browser versions. What do you think? Will you be using this feature, and are you looking forward to it becoming available? Let me know with a comment! 💜

Top comments (2)

Collapse
 
alohci profile image
Nicholas Stimpson

The big question is whether weak scoping will be enough. i.e. Specificity beats scope. The scope rules will only apply if their specificity is greater than or equal to the general document rules. Personally, I'll have to try it and get a feel for it to know whether it's a useful addition.

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

Currently, it's still an open question whether scope proximity should beat selector specificity in case of conflicting style rules, but for selectors of the same specificity at least, the scoped rules would take precedence.

Personally, I believe the issue of specificity would be fixed with CSS Layers once those become a thing, so scopes don't need to address this problem for the time being.

There's also the question of what role scope proximity should play in the case of nested scopes with conflicting rules. Should normal specificity decide which rule wins, or should the innermost scope overrule all others?

Once again, layers would fix this problem, but so would proper use of lower scope boundaries.