Skip to content

[css-scoping] Proposal for light-dom scoping/namespacing with re-designed @scope rule #5809

@mirisuzanne

Description

@mirisuzanne

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:

  1. Total isolation of a DOM subtree/fragment from the host page, so that no selectors get in or out unless explicitly requested.
  2. 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!

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions