[go: up one dir, main page]

Skip to content
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

Closed
Rich-Harris opened this issue Sep 3, 2021 · 21 comments
Closed

[css-scoping] Inclusive vs exclusive lower boundary #6577

Rich-Harris opened this issue Sep 3, 2021 · 21 comments

Comments

@Rich-Harris
Copy link
Rich-Harris commented Sep 3, 2021

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

@DarkWiiPlayer
Copy link

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
Copy link
Contributor

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
Copy link

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
Copy link
Contributor

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 :boundary pseudo-class' (actual name tbd). Then:

@scope (.component) to (.lower) {
  div { /* does not match div.lower */ }
  div:boundary { /* does match div.lower */ }
}

@DarkWiiPlayer
Copy link
@scope (.component) to (.lower) {
  div { /* does not match div.lower */ }
  div:boundary { /* does match div.lower */ }
}

Would that div:boundary only select the div.lower then? If so, wouldn't that mean one would have to duplicate every selector, like outer-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
Copy link
Author

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 exclusive keyword...

@scope ([data-scope='main-component']) to ([data-scope]) exclusive {...}

...or, if exclusive were 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):

/* this... */
@scope ([data-scope='main-component']) to ([data-scope]) inclusive {...}

/* ...is sugar for this */
@scope ([data-scope='main-component']) to ([data-scope] > *) {...}

@DarkWiiPlayer
Copy link
DarkWiiPlayer commented Nov 17, 2021

I like the idea of having an inclusive keyword, but it seems very easy to miss at the end; maybe it would be better like this:

@scope ([data-scope='main-component']) to inclusive ([data-scope]) {...}

This is a bit easier to figure out than to vs. until which seem a bit harder to remember which is which. I also think making exclusive the default makes a lot of sense.

@mirisuzanne
Copy link
Contributor

I agree that keywords are likely the clearest path forward here.

@DarkWiiPlayer
Copy link
DarkWiiPlayer commented Jan 25, 2022

So the options we currently have are:

  1. @scope (.a) to (.b) vs. @scope (.a) until (.b)
  2. @scope (.a) to (.b) inclusive vs. @scope (.a) to (.b) exclusive
  3. @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:

  1. For myself I'd choose this one, as it's shortest and saves unnecessary typing, but "to" and "until" are often used interchangeably in casual language so this may also be the most confusing.
  2. This one is definitely easier to remember as the words have very clear definitions, but I can't find any specific advantage to putting these keywords at the end.
  3. I slightly lean towards this one because it makes the keyword a bit more visible when reading the rule.

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
Copy link
Contributor

I also think option 3 makes the most sense. Agenda+ to see if we can resolve on that (or one of the others).

@bkardell
Copy link
Contributor
bkardell commented Mar 9, 2022

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 .a or .b, I wonder if it gets just a little harder to scan them if there is some 'noise' between them that is kind of secondary. It's pretty minor, probably, but because of this I sort of think I prefer them in the order they are listed, pretty much.

@mirisuzanne
Copy link
Contributor

If we keep the 'Selector Scoping Notation' we would also need to solve this problem in that syntax, which is currently ( <scope-start> [/ <scope-end>]? ).

@fantasai
Copy link
Collaborator

@mirisuzanne Would be nice if we can keep the two syntaxes somehow consistent.

@mirisuzanne
Copy link
Contributor

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:

  1. A different symbol for inclusive/exclusive (for example, using / for inclusive and // for exclusive)
/* inclusive scope rule */
@scope ([data-scope='main-component'] / [data-scope]) { .a { ... } }

/* exclusive selector syntax */
([data-scope='main-component'] // [data-scope]) .a { ... }
  1. A function name on the outer parenthesis
/* 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.

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed Inclusive vs exclusive lower boundary.

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

@astearns astearns removed the Agenda+ label Mar 23, 2022
@DarkWiiPlayer
Copy link
DarkWiiPlayer commented Mar 24, 2022

Just a quick idea: Considering how (foo) to inclusive (bar) is the same as (foo) to exclusive (bar) > *, it might be easier for the short hand syntax using slash to simply use that instead:

(.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 @scope rule with to as a separator, so there's also less reason to express exclusivity vs. inclusivity with an expressive keyword.

And > * has the advantage of already being a known CSS idiom, which imo makes it better than the / vs // distinction which is completely new.

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 to keyword. On the other hand, / is used to mean "to" in other places already, like in grid grid-row / grid-column.

Tangent regarding grid

One minor detail here: When using grid-row or grid-column, one technically doesn't work with rows/columns but with grid lines; however, when using numeric values, these actually correspond to actual row/column numbers with an exclusive second value.

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 / to also be exclusive in the case of scope.


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 > mimics the > * to hint that it includes one direct child. I still hate it, but a lot less than // which gives you no hint whatsoever how it differs from single slash.

@mirisuzanne
Copy link
Contributor

I'd like to bring this back for a proposed resolution on the following:

  • The default behavior is for lower boundaries to be excluded from scope
  • We will provide inclusive and exclusive keywords in the @scope syntax
  • The keyword comes after the selectors

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.

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed lower boundaries, and agreed to the following:

  • RESOLVED: make exclusive lower boundaries the default and add an example to the spec on how to do inclusive
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

@mirisuzanne
Copy link
Contributor

For now:

  • lower boundaries are excluded from a scope
  • authors can use .lower-boundary > * to generate 'inclusive' lower boundaries
  • additional syntax can be added if there is developer need

Closing this issue. We can open a new issue for the keywords (or reopen this) if we decide there is enough need.

@DarkWiiPlayer
Copy link
DarkWiiPlayer commented 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

@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?

@mirisuzanne
Copy link
Contributor

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.

chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this issue Feb 21, 2023
w3c/csswg-drafts#6577

Fixed: 1417896
Change-Id: I4c51177df78eba71cbbfacb88257bdee91b2039b
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this issue Feb 21, 2023
w3c/csswg-drafts#6577

Fixed: 1417896
Change-Id: I4c51177df78eba71cbbfacb88257bdee91b2039b
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this issue Feb 22, 2023
w3c/csswg-drafts#6577

Fixed: 1417896
Change-Id: I4c51177df78eba71cbbfacb88257bdee91b2039b
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this issue Feb 22, 2023
w3c/csswg-drafts#6577

Fixed: 1417896
Change-Id: I4c51177df78eba71cbbfacb88257bdee91b2039b
aarongable pushed a commit to chromium/chromium that referenced this issue Feb 22, 2023
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}
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this issue Feb 22, 2023
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}
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this issue Feb 22, 2023
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}
moz-v2v-gh pushed a commit to mozilla/gecko-dev that referenced this issue Mar 7, 2023
…=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
marcoscaceres pushed a commit to web-platform-tests/wpt that referenced this issue Mar 28, 2023
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}
aosmond pushed a commit to aosmond/gecko that referenced this issue May 18, 2023
…=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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants