-
Notifications
You must be signed in to change notification settings - Fork 785
[css-scoping] Proposal for light-dom scoping/namespacing with re-designed @scope rule #5809
Description
This would likely require additions to both CSS Scoping and CSS Cascade. See my full explainer for more details.
As I've been working on proposals around cascade layers & component queries, there is another aspect of "cascade modernization" that comes up regularly: scope. I'm aware that there is some hesitancy on the issue, since the initial specification was never implemented, and Shadow DOM was seen as a path forward (potentially a replacement). I think that time (and further development of Shadow-DOM) has helped clarify two quite different use-cases:
- Total isolation of a DOM subtree/fragment from the host page, so that no selectors get in or out unless explicitly requested.
- Lighter-touch component namespacing, and prioritization of "proximity" when resolving the cascade.
Shadow-DOM addresses the first, but it comes with a lot of overhead that is required for "full isolation". Meanwhile authors rely on convoluted naming conventions (like BEM) and JS tooling (such as CSS Modules, Styled Components, & Vue Scoped Styles) for the second use-case… which has been thoroughly discussed in various forms:
[Note: This doesn't attempt to resolve all the use-cases discussed in those threads. The discussion so far has often conflated the two approaches to scope, and I'm trying to divide them out. I think that still leaves a number of "isolation-first" cases that would best be addressed with changes that build on top of shadow-DOM - such as these ideas explored by Yu Han.]
Re-introducing @scope <selector> { ... } with a few adjustments…
1. Provide a "lower boundary" or "slot" syntax
This would make it possible to scope fragments rather than entire DOM sub-trees. @giuseppeg has suggested a syntax that I think is a good starting-point for more bikeshed discussion:
@scope (from: .carousel) and (to: .carousel-slide-content) {
p { color: red }
}In my mind, only the first ("from") clause should be required, and may not need explicit labeling. It would likely accept a single (complex) selector:
@scope (.media-block) {
img { border-radius: 50%; }
}In terms of selector-matching, this would be the same as .media-block img, but with slightly different cascade implications (see below). The second ("to") clause would be optional, and accept a list of selectors that represent lower-boundary "slots" in the scope. The targeted lower-boundary elements are included in the scope, but their descendants are not:
@scope (.media-block) to (.content) {
img { border-radius: 50%; }
.content { padding: 1em; }
}Which would only match img and .content inside .media-block -- but not if there are intervening .content between the scope root and selector target. This follows the current selector-scoping behavior of various popular tools.
I'm not convinced that to is necessarily the right keyword (others have proposed until) or if we should even consider using a functional syntax, or calling calling the lower boundary "slots":
@scope root(.media-block) slots(.content) { /* ... */ }More discussion would be useful.
2. Make the cascade effects of scoping much less intrusive (weighted below specificity)
When scopes do overlap, it's useful to recognize the proximity of a scope (inner scope takes precedence) in the cascade. This is not currently represented in CSS. Descendant selectors rely on source order rather than proximity:
/* link colors for light and dark backgrounds */
.light-theme a { color: purple; }
.dark-theme a { color: plum; }When these color themes are nested, the dark theme will always take precedence:
<div class="dark-theme">
<a href="#">plum</a>
<div class="light-theme">
<a href="#">also plum???</a>
</div>
</div>Both shadow DOM and the original spec give scoped context a very powerful impact on the cascade — overriding even specificity. The original spec also inverted scope-layering for !important declarations. This follows the logic of more highly-isolated use-cases, where there is more clear distinction between the inner scope and the outer host. But in the more common lightly-scoped cases, a more nuanced interplay between specificity and scope is helpful. Most existing tools only add minimal cascade weight to scoped selectors, like a single attribute selector.
I propose re-adding "scope proximity" to the cascade specification after/below selector specificity, but above/before source-order. That would help resolve our example above:
@scope (.light-theme) {
a { color: purple; }
}
@scope (.dark-theme) {
a { color: plum; }
}<div class="dark-theme">
<a href="#">plum</a>
<div class="light-theme">
<a href="#">purple</a>
</div>
</div>While still allowing more specific selectors to override scope when desired.
If authors desire more layering impact similar to the initial spec, that is now available using Cascade Layers — and the two features can be combined.
A path forward
This still needs a lot of work, but my goal here is to open discussion around a path forward for light-DOM scope/namespacing. I have a much more detailed explainer for my thought-process — but there are a lot of open questions, and I'd like to:
- gauge CSSWG interest before going deeper
- get more people involved with fleshing out the details
Happy for comments, thanks!