Repository navigation
Description
https://drafts.csswg.org/css-scoping-2/ describes the possible use of CSS Scoping by component frameworks, with the following example:
@scope ([data-scope='main-component']) to ([data-scope]) {...}Since the lower boundary is inclusive, this matches the behaviour of the scoped styles in Vue Single-File Components: CSS declared in an SFC's <style> element apply to all elements of the current component, plus the root element of child components.
But not all userland scoping systems work this way. Svelte has stricter encapsulation: by design, styles declared in one component can't affect a child component at all unless you opt-in to that behaviour with the :global(...) modifier (demo):
<div>
<p>red on yellow</p>
<Widget/>
</div>
<style>
/* this will affect any <p> elements inside <Widget/> or its children
in addition to this component's elements */
div :global(p) {
text-decoration: underline;
}
/* these styles only affect the <div> and the <p> above, regardless of
whether the selectors match top-level elements in <Widget/> */
div {
background-color: yellow;
}
p {
color: red;
}
</style>Svelte compiles this to the following CSS:
div.svelte-185puzw p {
text-decoration: underline;
}
div.svelte-185puzw {
background-color: yellow;
}
p.svelte-185puzw {
color: red;
}The above suggested approach would look like this...
@scope ([data-scope='main-component']) to ([data-scope]) {
div p {
text-decoration: underline;
}
div {
background-color: yellow;
}
p {
color: red;
}
}...which would incorrectly apply color: red to <p data-scope="sub-component">.
We on the Svelte team would love to be able to use CSS Scoping one day, but we think it's important that a component's styles don't leak into its children unless the author explicitly opts in. If the lower boundary is inclusive, then as far as I can see we would have to do something like this...
@scope ([data-scope='main-component']) to (:not([data-scope='main-component'])) {...}...and also apply the data-scope="main-component" attribute to every element. It's not clear that this would be an improvement on the current situation.
Is there a way we might only apply styles until the lower boundary is reached? For example:
@scope ([data-scope='main-component']) until ([data-scope]) {...}@scope ([data-scope='main-component']) to ([data-scope]) exclusive {...}More controversially, perhaps the lower boundary should always be exclusive? It's worth noting that you can express the current semantics with an exclusive lower boundary...
@scope ([data-scope='main-component']) to ([data-scope] > *) {...}...but the reverse isn't true, which to my mind is a strong argument in favour — it gives all authors more expressive power.
edited the final code snippet to remove :not(:scope) — on a closer reading of the explainer, the lower boundary selector only matches descendants of the upper boundary
Activity
DarkWiiPlayer commentedon Nov 8, 2021
It took me a while to even get what the point of this issue is because I just assumed that
to <selector>would mean<selector>is not included in the scope anymore. I definitely think the lower boundary should be exclusive.mirisuzanne commentedon Nov 12, 2021
It doesn't seem to me that there is a universally right choice for this. In most of my CSS use-cases, I would want to define boundaries that are part of the scope (inclusive - also used by Vue), but it's also clear the exclusive approach makes more sense with some component-nesting cases. I hope we don't end up supporting one over the other. We need the choice available for authors to express both
I like the fact that exclusive boundaries leave that choice open to an extent (via
boundary > *selectors), but I think we might want some more explicit way of expressing that choice.DarkWiiPlayer commentedon Nov 16, 2021
For every application I have for CSS scopes, it would make more sense to have an exclusive lower boundary. Usually one knows the first element down the tree that should be outside the scope, like a nested component.
But I also think that both options should be available. While
boundary > *would semantically be equivalent to an inclusive boundary, giving it its own keyword would both make the code look nicer and signal to the browsers that both options should be reasonably optimised (I can imagine the direct child selector being slower when not specifically optimised)However, the obvious question is whether mixing the two should be allowed; that is, having a scope end exclusively at one selector or inclusively at another.
Say we wanted to exclude anything with an ID from the scope, and any child element from a nested component, but while keeping the component inside the scope; would this be valid:
@scope (my-component) to (container-component) until ([id])?Of course, there'd still be the fallback of doing
@scope (my-component) until ([id], container-component>*)in that case, but that has the same problems as not having an implicit boundary in general.mirisuzanne commentedon Nov 16, 2021
Another way to approach this that would allow for both-at-once is to say 'lower boundaries are exclusive by default unless explicitly marked with the
:boundarypseudo-class' (actual name tbd). Then:DarkWiiPlayer commentedon Nov 17, 2021
Would that
div:boundaryonly select thediv.lowerthen? If so, wouldn't that mean one would have to duplicate every selector, likeouter-element > inner-element.some-class + p, outer-element > inner-element.some-class + p:boundary?But this also leads to another question: Should boundary inclusion be defined per boundary, or per selector?
Rich-Harris commentedon Nov 17, 2021
Personally I would argue that the pseudo-class approach is a bit counterintuitive, since normally a pseudo-class narrows a selector rather than expanding it:
I would find it much more natural if it was part of the scope itself — either with something like an
exclusivekeyword......or, if
exclusivewere the default (which I would definitely advocate for, partly given my own use cases but mostly because inclusive semantics can be expressed with exclusive semantics but not vice versa):DarkWiiPlayer commentedon Nov 17, 2021
I like the idea of having an
inclusivekeyword, but it seems very easy to miss at the end; maybe it would be better like this:This is a bit easier to figure out than
tovs.untilwhich seem a bit harder to remember which is which. I also think making exclusive the default makes a lot of sense.mirisuzanne commentedon Nov 18, 2021
I agree that keywords are likely the clearest path forward here.
DarkWiiPlayer commentedon Jan 25, 2022
So the options we currently have are:
@scope (.a) to (.b)vs.@scope (.a) until (.b)@scope (.a) to (.b) inclusivevs.@scope (.a) to (.b) exclusive@scope (.a) to inclusive (.b)vs.@scope (.a) to exclusive (.b)Leaving aside the question of which should be the default in 2. and 3., here's my thoughts on each of them:
In the case of 2. and 3., the question of which should be the default when neither keyword is used could be decided at a later moment. That way the question could already be narrowed down a lot.
mirisuzanne commentedon Jan 25, 2022
I also think option 3 makes the most sense. Agenda+ to see if we can resolve on that (or one of the others).
13 remaining items
mirisuzanne commentedon Sep 19, 2022
For now:
.lower-boundary > *to generate 'inclusive' lower boundariesClosing this issue. We can open a new issue for the keywords (or reopen this) if we decide there is enough need.
DarkWiiPlayer commentedon Sep 21, 2022
Just throwing in another random thought:
Should there be some explicit way of styling the lower boundary?
Something along the lines of
Is there a better way around it with what we currently have? Is this worth opening its own issue for?
mirisuzanne commentedon Sep 21, 2022
I think for now the solution is defining multiple scopes:
Similar to keywords for exclusive/inclusive, we could consider extra syntax sugar for that case once we have a better sense how the feature is being used.
[@scope] Make scoping limit exclusive
[@scope] Make scoping limit exclusive
[@scope] Make scoping limit exclusive
[@scope] Make scoping limit exclusive
[@scope] Make scoping limit exclusive
[@scope] Make scoping limit exclusive
[@scope] Make scoping limit exclusive
Bug 1818018 [wpt PR 38611] - [@scope] Make scoping limit exclusive, a…
[@scope] Make scoping limit exclusive
Bug 1818018 [wpt PR 38611] - [@scope] Make scoping limit exclusive, a…
[@scope] Make scoping limit exclusive