Day 101: scoping
posted on
It’s time to get me up to speed with modern CSS. There’s so much new in CSS that I know too little about. To change that I’ve started #100DaysOfMoreOrLessModernCSS. Why more or less modern CSS? Because some topics will be about cutting-edge features, while other stuff has been around for quite a while already, but I just have little to no experience with it.
Similar to container queries or cascade layers, we have another new impactful feature in CSS: scoping.
Let's start nice and easy by reading the spec.
A scope is a subtree or fragment of a document, which can be used by selectors for more targeted matching.
More targeted matching sounds great. We can use the @scope
rule to scope styles of a child to a parent selector.
<div class="wrapper">
<div class="card">
<div class="content">
the cascade is unavoidable
</div>
</div>
</div>
@scope(.card) {
.content {
border: 5px solid red;
}
}
Okay, cool, but we could do that already, right? By combining selectors, we can scope a child to a parent.
.card .content {
border: 5px solid red;
}
The biggest difference in this simple example is that the cascade prioritizes declarations with a more proximate scoping root, regardless of specificity or source order.
The background color will be blue in the following example because of the order of appearance.
.card .content {
background-color: green;
}
.wrapper .content {
background-color: blue;
}
/* -> blue */
In the following example, it's green because before the order of appearance can say anything, scope proximity compares declarations that appear in style rules with different scoping roots and picks the declaration with the fewest generational or sibling-element hops between the scoping root (.card
or .wrapper
) and the scoped style rule subject (.content
). In other words: the cascade prioritizes declarations with a more proximate scoping root.
@scope (.card) {
.content {
background: green;
}
}
@scope (.wrapper) {
.content {
background: blue;
}
}
/* -> green */
There's more to proximity than that, but we'll save that for another day.
That's not everything. @scope
allows us to define scope limits, which I'll cover on another day. That's where it gets really interesting, but before we can talk about that, here are a couple of things to note:
-
Selectors within a scoped style rule can only match elements that are in scope, but this applies only to the subject (the actual selector that matches the element). Any other selector in the selector list can be out of scope.
@scope(.card) { /* `.content` (the subject) must be in scope `body` is not in scope and doesn't have to */ body & .content { border: 5px solid red; } }
-
Selectors within a scope block are relative to the scope.
@scope(.card) { /* works with the markup structure at the beginning of this post because it selects `.card .content` */ .content { } /* doesn't work because it selects `.card .card .content` */ .card .content { } }
-
Unlike Nesting, selectors within an scoped style rule do not acquire the specificity of any parent selector(s) in the
@scope
prelude.@scope(.card) { /* the specificity of .content is (0, 1, 0) */ .content { } }
To try out @scope
you have to download Chrome Canary and enable the Experimental Web Platform features
flag in chrome://flags/.
Further reading
Overview: 100 Days Of More Or Less Modern CSS