-
Notifications
You must be signed in to change notification settings - Fork 673
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[css-scoping] Inclusive vs exclusive lower boundary #6577
Comments
It took me a while to even get what the point of this issue is because I just assumed that |
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 |
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 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: Of course, there'd still be the fallback of doing |
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 @scope (.component) to (.lower) {
div { /* does not match div.lower */ }
div:boundary { /* does match div.lower */ }
} |
Would that But this also leads to another question: Should boundary inclusion be defined per boundary, or per selector? |
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: div { /* matches all divs */ }
div:last-child { /* narrows selection to divs that are the last child */ } I would find it much more natural if it was part of the scope itself — either with something like an @scope ([data-scope='main-component']) to ([data-scope]) exclusive {...} ...or, if /* this... */
@scope ([data-scope='main-component']) to ([data-scope]) inclusive {...}
/* ...is sugar for this */
@scope ([data-scope='main-component']) to ([data-scope] > *) {...} |
I like the idea of having an @scope ([data-scope='main-component']) to inclusive ([data-scope]) {...} This is a bit easier to figure out than |
I agree that keywords are likely the clearest path forward here. |
So the options we currently have are:
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. |
I also think option 3 makes the most sense. Agenda+ to see if we can resolve on that (or one of the others). |
I don't have very strong feelings about this, any of 1,2,3 seem totally usable to me, but... When you're looking at a range, inclusive or exclusive feels secondary to the selectors, which will in all probability look a lot more like @Rich-Harris's examples - as those get more complex than |
If we keep the 'Selector Scoping Notation' we would also need to solve this problem in that syntax, which is currently |
@mirisuzanne Would be nice if we can keep the two syntaxes somehow consistent. |
I agree. We could technically merge the two syntaxes completely. The 'Selector Scoping Notation' would fit fine in an at-rule (the other direction is a bit more difficult). In that case we don't have parenthesis to separate the two selector lists, and keywords (to/until) are indistinguishable from element selectors, so we use a symbol. The two most obvious solutions from there are either:
/* inclusive scope rule */
@scope ([data-scope='main-component'] / [data-scope]) { .a { ... } }
/* exclusive selector syntax */
([data-scope='main-component'] // [data-scope]) .a { ... }
/* inclusive scope rule */
@scope inclusive([data-scope='main-component'] / [data-scope]) { .a { ... } }
/* exclusive selector syntax */
exclusive([data-scope='main-component'] / [data-scope]) .a { ... } Since the selector syntax requires a symbol anyway, I lean towards giving that symbol some power (option 1) to keep things compact. But option 2 has the advantage of being more explicit. Also open to other options here. |
The CSS Working Group just discussed The full IRC log of that discussion<fremy> Topic: Inclusive vs exclusive lower boundary<astearns> github: https://github.com//issues/6577 <fremy> miriam: the scoping proposal takes two selector lists (the second one is optional) <fremy> miriam: the first one species when the scope starts <fremy> miriam: the second one specifies from which nested elements the scope stops <fremy> miriam: scoping views include lower boundaries in the scope <fremy> miriam: but sometimes the boundaries are excluded of the scope <fremy> miriam: there are two options, and feedback is that both might be useful <fremy> miriam: there is a proposal for a syntax <fremy> miriam: but me and fantasai where willing for the two syntaxes to look similar <fremy> miriam: it's not clear yet if both options are needed <fremy> miriam: a few comments from the bottom, there are a few proposals <TabAtkins> q+ <astearns> vastly prefers a keyword over making the number of slashes significant <fremy> miriam: depending if they are in the same parenthesis or not, we can use a slash or not <fremy> miriam: one question is that, should we add a keyword for the exclusivity <astearns> ack TabAtkins <fremy> miriam: or a special separator <fremy> TabAtkins: I am weakly in favor of allowing both <fremy> TabAtkins: our current default sounds reasonable though <fremy> TabAtkins: I don't like slash vs double-slash <fremy> TabAtkins: because it's not understandable at a glance <fremy> TabAtkins: I would rather add a modifier for the other behavior <astearns> ack fantasai <fremy> fantasai: the issue with keywords, is that they could be part of the selector <fremy> TabAtkins: then we should put a function around the lower bound maybe? <fremy> fantasai: I think we should explore this further <fremy> astearns: are there reservations about adding this choice at all? <fremy> astearns: sounds like not <fremy> astearns: let's take it back to the issue for future iterations <fremy> miriam: another thing, do we want to merge the two syntaxes into one that works in both places <fremy> TabAtkins: I haven't looked into this much, but that sounds like a good goal to have <fremy> astearns: seems like it would be nice need if it's possible, indeed <fremy> astearns: slight differences are a possible trade-offs though <fremy> miriam: sounds good, thanks for the feedback, we will take it back in the thread |
Just a quick idea: Considering how (.post / .comment) .title { font-size: 2em; } /* Exclusive */
(.post / .comment > *) .title { font-size: 2em; } /* Inclusive */ So I'm starting to think that maybe selector scope notation should always be exclusive. For one, the shorter syntax already looks a lot more esoteric than a And As for the idea of using the slash syntax for @scope rules, while I like the consistency that would bring, I really hate how esoteric the slash looks compared to the Tangent regarding gridOne minor detail here: When using For example: p { grid-column 1 / 2; } could be thought of as "From column 1 to excluding column 2". This is also how I think of these values, despite knowing that that's not entirely accurate. Because of that, it would feel more consistent for So here's what I would currently consider ideal: @scope (outer-component) to (inner-component) {}
/* Default exclusive, to be consistent with selector notation */
@scope (outer-component) to exclusive (inner-component) {}
/* Redundant, as exclusive would be default, but more explicit for devs who don't want to bother remembering the default */
@scope (outer-component) to inclusive (inner-component) {}
(outer-component / inner-component) h1 { font-size: 2em } /* Exclusive by default */
(outer-component / inner-component > *) * { color: red; } And, just to throw this idea in the room(outer-component /> inner-component) * { color: red; } It's not what I would consider pretty, nor excessively easy to visually parse nor understand; but at least the |
I'd like to bring this back for a proposed resolution on the following:
Depending on the resolution of #7709, the selector scoping notation may become irrelevant, or it can be handled manually as shown by @DarkWiiPlayer in the previous comment. I don't believe this should be blocked because we can't think of a shorthand for something that's already possible in a syntax we're not even sure we need. |
The CSS Working Group just discussed
The full IRC log of that discussion<emilio> topic: lower boundaries<emilio> github: https://github.com//issues/6577 <emilio> miriam: with the lower boundary selector as part of the scope, there's a question of whether they're part or not of the scope <emilio> ... there's use cases for both, initially we spec'd as inclusive <emilio> ... so lower boundaries are part of the scope <fantasai> https://github.com//issues/6577#issuecomment-1021035991 <emilio> ... exclusive allows to include explicitly with `> *` <emilio> ... we want to add a keyword to `@scope` <TabAtkins> q+ <emilio> ... to say whether you want the boundaries included <flackr> q+ <Rossen_> ack TabAtkins <emilio> TabAtkins: I'm fine with the kw <emilio> ... but I think exclusive is better default <emilio> ... also it's trivial to turn exclusive into inclusive <Rossen_> ack flackr <emilio> flackr: tab covered what I was going to say <florian> q+ <emilio> ... exclusive is a little bit more ergonomic <Rossen_> ack florian <emilio> florian: does `> *` work if there's no child? <emilio> TabAtkins: if no element matches the lower bound nothing gets excluded which is what you want <emilio> miriam: I guess main argument to have it a keyword is readability, is it clear? <emilio> TabAtkins: I think it does what it says and says what it does <emilio> ... inclusive / exclusive ranges are a perennial source of confusion in every context <emilio> fantasai: we can always add a keyword if we want to <emilio> miriam: So proposal is make exclusive the default and add an example to the spec on how to do inclusive right? <emilio> fantasai: yes, is default for upper bound inclusive? <TabAtkins> they're both inclusive, the lower bound just includes elements in the forbid list ^_^ <emilio> miriam: yeah, and no use cases for exclusive <emilio> RESOLVED: make exclusive lower boundaries the default and add an example to the spec on how to do inclusive <emilio> miriam: aside, I want people to look at how proximity affects cascade priority <emilio> ... not to discuss today <miriam> https://github.com//issues/6790 |
For now:
Closing this issue. We can open a new issue for the keywords (or reopen this) if we decide there is enough need. |
Just throwing in another random thought: Should there be some explicit way of styling the lower boundary? Something along the lines of @scope ([data-scope="main-component"]) to ([data-scope]) {
/* Generally, we do not want to style the lower boundary; probably the most common case */
* { color: red; }
/* occasionally we might want to explicitly include the lower boundary too */
*+*, *+:lower-boundary { margin-top: 1em; }
} Is there a better way around it with what we currently have? Is this worth opening its own issue for? |
I think for now the solution is defining multiple scopes: /* Generally, we do not want to style the lower boundary; probably the most common case */
@scope ([data-scope="main-component"]) to ([data-scope]) {
* { color: red; }
/* occasionally we might want to explicitly include the lower boundary too */
*+*, *+:lower-boundary { margin-top: 1em; }
}
/* occasionally we might want to explicitly include the lower boundary too */
@scope ([data-scope="main-component"]) to ([data-scope] > *) {
*+* { margin-top: 1em; }
} 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. |
w3c/csswg-drafts#6577 Fixed: 1417896 Change-Id: I4c51177df78eba71cbbfacb88257bdee91b2039b
w3c/csswg-drafts#6577 Fixed: 1417896 Change-Id: I4c51177df78eba71cbbfacb88257bdee91b2039b
w3c/csswg-drafts#6577 Fixed: 1417896 Change-Id: I4c51177df78eba71cbbfacb88257bdee91b2039b
w3c/csswg-drafts#6577 Fixed: 1417896 Change-Id: I4c51177df78eba71cbbfacb88257bdee91b2039b
w3c/csswg-drafts#6577 Fixed: 1417896 Change-Id: I4c51177df78eba71cbbfacb88257bdee91b2039b Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4272283 Reviewed-by: Rune Lillesveen <futhark@chromium.org> Commit-Queue: Anders Hartvoll Ruud <andruud@chromium.org> Cr-Commit-Position: refs/heads/main@{#1108286}
w3c/csswg-drafts#6577 Fixed: 1417896 Change-Id: I4c51177df78eba71cbbfacb88257bdee91b2039b Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4272283 Reviewed-by: Rune Lillesveen <futhark@chromium.org> Commit-Queue: Anders Hartvoll Ruud <andruud@chromium.org> Cr-Commit-Position: refs/heads/main@{#1108286}
w3c/csswg-drafts#6577 Fixed: 1417896 Change-Id: I4c51177df78eba71cbbfacb88257bdee91b2039b Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4272283 Reviewed-by: Rune Lillesveen <futhark@chromium.org> Commit-Queue: Anders Hartvoll Ruud <andruud@chromium.org> Cr-Commit-Position: refs/heads/main@{#1108286}
…=testonly Automatic update from web-platform-tests [@scope] Make scoping limit exclusive w3c/csswg-drafts#6577 Fixed: 1417896 Change-Id: I4c51177df78eba71cbbfacb88257bdee91b2039b Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4272283 Reviewed-by: Rune Lillesveen <futhark@chromium.org> Commit-Queue: Anders Hartvoll Ruud <andruud@chromium.org> Cr-Commit-Position: refs/heads/main@{#1108286} -- wpt-commits: c8ee8829ffe92a484644a4d1806e124eef70bf29 wpt-pr: 38611
w3c/csswg-drafts#6577 Fixed: 1417896 Change-Id: I4c51177df78eba71cbbfacb88257bdee91b2039b Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4272283 Reviewed-by: Rune Lillesveen <futhark@chromium.org> Commit-Queue: Anders Hartvoll Ruud <andruud@chromium.org> Cr-Commit-Position: refs/heads/main@{#1108286}
…=testonly Automatic update from web-platform-tests [@scope] Make scoping limit exclusive w3c/csswg-drafts#6577 Fixed: 1417896 Change-Id: I4c51177df78eba71cbbfacb88257bdee91b2039b Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4272283 Reviewed-by: Rune Lillesveen <futhark@chromium.org> Commit-Queue: Anders Hartvoll Ruud <andruud@chromium.org> Cr-Commit-Position: refs/heads/main@{#1108286} -- wpt-commits: c8ee8829ffe92a484644a4d1806e124eef70bf29 wpt-pr: 38611
https://drafts.csswg.org/css-scoping-2/ describes the possible use of CSS Scoping by component frameworks, with the following example:
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):Svelte compiles this to the following CSS:
The above suggested approach would look like this...
...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...
...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:
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...
...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 boundaryThe text was updated successfully, but these errors were encountered: