w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.5k stars 661 forks source link

[css-cascade-6] Should the scope proximity calculation be impacted by nesting scopes? #10795

Open mirisuzanne opened 2 months ago

mirisuzanne commented 2 months ago

Background:

The published definition of 'scope proximity' states that:

If two declarations both have elements selected by scoped descendant relationships applying weak scoping proximity, then the declaration with the fewest generational hops between the ancestor/descendant element pair wins.

If multiple such pairs are represented, their weak scoping proximity weights are compared from innermost scoping relationship to outermost scoping relationship (with any missing pairs weighted as infinity).

However, in the Editor's Draft, the second paragraph was removed and the first paragraph adjusted, so that each scoped selector has one single scope root and a single proximity number.

In our publishing discussion last week, @mdubet asked to reconsider this.

How it might work:

In order to find a 'proximity', we need both a 'subject' element and a ':scope' element. Then we count the 'steps' between one and the other.

Nested @scope rules are allowed. Each scope rule's <scope-start> selector is 'scoped' by the parent scope rule. If we want scopes to accumulate with nesting, we have to determine which subjects we are comparing to which roots. Given this example:

@scope (a) {
  @scope (b) { 
    c { /* … */ }
  }
}

I see two options (though I believe they might be functionally the same??). The scope proximity weight for c is one of:

In either case, I believe the proposal is to compare proximities from inner-most to outer-most.

But why?

I think this would be a reasonable approach. At least, it makes some sense to me that things might work this way. But I can't think of an actual use-case where I would rely on this behavior. I'm not opposed, but I'm also not sure how useful or complex it is.

mirisuzanne commented 2 months ago

Thinking through it a bit more, and discussing with @argyleink, I don't really have any reason not to do this. The alternative is falling back on source order, which isn't better than multi-step proximity.

And I believe there's no difference between the two approaches above. The distance between two roots will always be equivalent to the additional distance between a subject and ancestor root. So the approach seems straight-forward.

So unless there's push back from other implementors (@andruud made the initial change here?), I'm going to propose we resolve on @mdubet's proposal here. Marking this as agenda+ to try and get that resolution.

andruud commented 2 months ago

@mirisuzanne In other words, this would introduce a dynamic number of cascade criteria (for the first time)? A bit like specificity, but instead of (A,B,C), it's a variable number of components.

But why? [...]

Last time this came up, we concluded that: 1) it adds complexity (both for impl and authors' mental model), and 2) it's not useful. Your answer to this question suggests that nothing has changed. Therefore, I do oppose this change, as it seems to (at best) only be about theoretical purity at the expense of other things.

I'm also not sure how [...] complex it is

We'd ideally investigate that a little bit before making any moves spec wise. @scope also shipped a long time ago in Blink. I would need to be able to prove that we even can ship such a change without breakage. Otherwise, we might end up with subtly different cascade behaviors forever, which is worse than just aligning on the current spec.

I'm going to propose we resolve on @mdubet's proposal here

We should minimally first answer the "But why?" with an actual answer, and explain why the more complex behavior is useful after all.

emilio commented 1 month ago

cc @dshin-moz

dshin-moz commented 1 month ago

So given something like

<div class="scope-2">
  <div><div><div>
    <div class="scope-1">
      <div class="styled"></div>
    </div>
  </div></div></div>
</div>

and given below rules:

@scope (.scope-2) {
  @scope(.scope-1) {
    .styled {
      background: blue;
    }
  }
}

/* Outer scope proximity, as per proposal, is infinity */
@scope (.scope-1) {
  .styled {
    background: green;
  }
}

The concern is that the applied .styled would depend purely on the order of declaration, right?

FWIW, authors that want this could coax this out by using & and relying on specificity, becoming very CSS Nesting-like:

@scope (.scope-2) {
  @scope(& .scope-1) {
    & .styled {
      background: blue;
    }
  }
}

@scope (.scope-1) {
  & .styled {
    background: green;
  }
}
dshin-moz commented 1 month ago

As for adding a dynamically-sized cascade criteria... I generally agree with @andruud - Concerned about complexity on implementations/authors. Could adding a count of nested @scope work as an approximation that does not require dynamic sizing?

mirisuzanne commented 4 weeks ago

To flesh that proposal out a bit, we'd have a (consistently) two-part value, including:

In your example, the first rule has a scope of [1,2] and the second has a scope of [1,1]. As with specificity, we would compare those one at a time - moving to the scope-count as a tie-breaker only when the proximity is equal.

I would be happy with that as an approximation.

andruud commented 4 weeks ago

(I said elsewhere that I'd look into the complexity and performance issues re. adding a dynamic number of cascade criteria, but I'm still working on that, so I won't comment on that yet.)

Could adding a count of nested @scope work as an approximation that does not require dynamic sizing?

I would be happy with that as an approximation.

That would be much more acceptable, so +1 from me.

I would also argue that it's better for authors to not over-complicate the cascade criteria even more, and outer scopes just adding a flat 1 to a single tie-breaker sounds like an easier model to manage mentally.

@mirisuzanne Once, you also believed in the benefit of keeping it simple here:

"In my mind proximity is a useful heuristic in the simple cases - and this logic [single proximity] continues to handle those cases well. Once things get more complicated, authors will likely need to think about other cascade controls: layers, specificity, etc. With a heuristic like this, I think it would be a mistake to get too clever about solving more complex scenarios in an abstract or magical way." [1]

"I don't see any reason to have a specificity-like cascade mechanic based on 'how many scopes were used to get here'. That would over-complicate what scope is about." [2]

I still haven't seen an actual reason to change anything here, but I can live with @dshin-moz' proposal in any case.

mirisuzanne commented 4 weeks ago

I do still think it's worth keeping this simple. I'm just happy to have the conversation - and want to make sure we're getting the right balance. Simple for authors to reason about is more important to me than simple for browsers to track. And I'm curious what makes the most sense to others.

emilio commented 4 weeks ago

+1 to cascade order being already complicated enough fwiw. I actually wonder if scope proximity is all that useful to begin with..

andruud commented 3 weeks ago

I've prototyped the original proposal in Blink, and there will be performance regressions if we do this. Not as severe as I feared, but still enough that I think we should strongly consider @dshin-moz' proposal instead. That behavior is also easier for authors to comprehend IMO.

Otherwise, we could actually consider removing proximity entirely (as @emilio is hinting at). I kind of regret not making @scope just about scoping.

romainmenke commented 3 weeks ago

Could adding a count of nested @scope work as an approximation that does not require dynamic sizing?

That also seems good to me.


I actually wonder if scope proximity is all that useful to begin with..

Otherwise, we could actually consider removing proximity entirely (as @emilio is hinting at). I kind of regret not making @scope just about scoping.

Proximity is actually the CSS feature we are most excited about.

It is very common for us to have components and layouts with areas that can contain other components. With both potentially having content from a wysiwyg editor. Many CMS's are moving towards very flexible page builders where content editors can nest components in various ways.

The only way to style these as designers would expect them to appear is by using @scope and if @scope has proximity.

An abstracted example of what we often encounter:

(I bet that component authors encounter similar issues but at a more granular/smaller level?)

I really hope that we don't lose proximity.

mirisuzanne commented 3 weeks ago

Yeah, I understand concerns about proximity being heuristic and associated with scope, but:

But I haven't actually seen any use-cases where you want one of these behaviors and don't want the other. I've only seen hand-wringing about it, and no examples of why it's not useful or should be separate.

I'm happy to have that conversation here or elsewhere. I also don't want to ship something if we don't think it will work. But I'm not sure how to respond when the concerns are never fleshed out beyond vague unease.

css-meeting-bot commented 2 weeks ago

The CSS Working Group just discussed [css-cascade-6] Should the scope proximity calculation be impacted by nesting scopes?.

The full IRC log of that discussion <TabAtkins> miriam: scopes add "proximity" to the cascade, which is the number of elements between the element and the scope root. closer proximity wins when there's a specificity tie
<TabAtkins> miriam: the only use-cases we've seen rely on just that final step
<andruud> q+
<TabAtkins> miriam: rather, author's interest seems to be mostly in just that final step, nested scopes seem difficult to track impl wise and Anders suggests it's a perf issue too
<TabAtkins> miriam: ??? was suggesting we could maybe have *some* way to tracking nested scopes
<astearns> s/???/matthieud
<TabAtkins> miriam: a proposal is a number that is the proximity distance, and number of scopes as a tiebreaker
<emilio> q+
<TabAtkins> miriam: I'd be happy with either leaving as is, or just going with scope count
<astearns> ack andruud
<emilio> q+ later
<emilio> ack emilio
<TabAtkins> andruud: the original proposal in the issue was a dynamic cascade criteria for proximity; i was really worried about the perf
<TabAtkins> andruud: it wasn't as bad as I thought. was still some perf regression tho.
<TabAtkins> andruud: so I still think that *if* we make a change, we shoudl go with the alternate proposal (just the count of scopes)
<TabAtkins> andruud: simpler for impl, and I think simpler for authors to understand
<astearns> ack fantasai
<TabAtkins> fantasai: i looked at the proposal; i had a question.
<TabAtkins> fantasai: how sure are we that want to index only on the last scope proximity
<TabAtkins> fantasai: would it make sense to consider which of the scopes is the "highest ranking"?
<TabAtkins> fantasai: i could imagine, depending on your stylesheet structure, one or the toher would be more important
<TabAtkins> miriam: would the importance be marked in some way?
<romain> having a scope-end makes it more important, in a way
<TabAtkins> fantasai: i dunno, could be an interesting qeustion
<romain> having both a scope-start and scope-end vs. only having a scope-start
<TabAtkins> fantasai: for the proximity distance, considering the highest rather than last could make sense ehre
<fantasai> s/highest/strongest/
<TabAtkins> andruud: we're talkinga bout nested scopes here. so isn't the innermost scope always closer to the subject than the outer scopes?
<TabAtkins> miriam: yes
<TabAtkins> miriam: the only way you'd change that is if you were measuring the distance between the scopes, but i don't know that that's useful
<TabAtkins> matthieud: that's the same as counting the subjects, yeah
<TabAtkins> andruud: the cascade is already really complex altogether, so I'd like to keep it as simple as possible
<matthieud> s/counting the/counting from the/
<bramus> +1
<TabAtkins> miriam: i tend to agree
<TabAtkins> +1, as simple as possible (but no simpler)
<TabAtkins> miriam: definitely use-cases for measuring to nearest scope. maybe some cases for measuring to nested scopes, but not really common, and not sure where it's where proximity is most useful
<astearns> ack emilio
<TabAtkins> emilio: was gonna bring same point as fantasai
<TabAtkins> emilio: accounting only for proximity fo the closest feels a little weird
<TabAtkins> emilio: leans me towards preferring no change if this isn't super useful
<TabAtkins> emilio: you can get this sort of tied behavior by using nested selector
<TabAtkins> emilio: using the & can pull in the specificity of the scope root
<matthieud> q+
<noamr> +1 to not adding implicit cascade rules on top of the existing ones
<TabAtkins> fantasai: the trick woudl be to come up with realistic examples where you're nesting scopes and caring about the proximity of each
<TabAtkins> fantasai: the canonical example of proximity is the light/dark switch, where you want to style based on which is the closest ancestor rather than stylesheet ordering
<TabAtkins> fantasai: and if you combine that with other aspects that you might want -- let's say you had a "zoom" switch as well
<TabAtkins> miriam: generally in that case they'll be written as separate scopes, not nested scopes
<TabAtkins> emilio: if you were to nest them, the behavior would be extremely confusing
<TabAtkins> I'm pretty strong on the side of "it woudl be extremely confusing" even if there is a use-case
<astearns> ack matthieud
<TabAtkins> matthieud: first question is about usage
<TabAtkins> matthieud: seems we don't have an idea even about the usage of nested scope itself
<TabAtkins> matthieud: shoudl we forbid nested scope altogether?
<TabAtkins> matthieud: second, about impl. Anders, not sure if it's very different from current impl.
<emilio> q+
<TabAtkins> matthieud: Seems linear to the number of scopes
<andruud> No
<TabAtkins> matthieud: Haven't done the impl but doesn't seem particularly complex. And perf concern seems only to be about nested scopes, falt scopes seems fine
<TabAtkins> emilio: The main problem is that this makes things that the cascade depends on dynamically sized, which means you need at least an extra pointer somewhere
<TabAtkins> emilio: And that's extremely hot code
<TabAtkins> emilio: so it's not the perf of looking for the scopes (agreed you already need to look thru them), it's about storing the list of appliable decl blocks
<TabAtkins> emilio: if you have those along with cascade order, you have to grow them, and it grows unconditionally regardless of whether you use it, it causes cache misses, etc
<TabAtkins> matthieud: okay if you alway shave an integer for scope is ee it. our impl always has a pointer
<miriam> q?
<TabAtkins> emilio: right, for us making ti a pointer would be a perf regression just going in
<miriam> q+
<astearns> ack emilio
<TabAtkins> emilio: so if it's mostly a theoretical purity thing we could avoid the perf hacks
<TabAtkins> andruud: I'll say again that perf *would* be acceptable if you *did* want to do it that way. I thought it would be terrible, but it's merely uncofmortable. Doable if *needed*, I just don't want to.
<astearns> ack miriam
<TabAtkins> miriam: responding to first part about disallowing nested
<TabAtkins> miriam: I don't think so
<TabAtkins> miriam: i know the reasons for nesting scopes - to narrow in on a selector
<TabAtkins> miriam: I haven't seen a use-case where that changes what proximity is doing
<TabAtkins> miriam: so to me i don't see it as necessary that that effects in some strong way
<TabAtkins> miriam: And the risk to disallowing entirely is, we'd have to remove scoping stylesheets, which we currently have on @import
<TabAtkins> I think the use-case for nested scopes is clear.
<TabAtkins> @scope my-component { @scope light {...}}
<astearns> ack fantasai
<TabAtkins> fantasai: so the question is what use-cases are there for nesting scopes
<TabAtkins> fantasai: would it make sense to have a blog post to collect feedback on this to get examples?
<fantasai> https://github.com/w3c/csswg-drafts/issues/8380#issuecomment-1450475188
<fantasai> @scope (A) {
<fantasai> @scope (B) {
<fantasai> X { color: blue }
<fantasai> }
<TabAtkins> fantasai: this is the preivous issue we had on the topic
<fantasai> X { color: yellow }
<TabAtkins> fantasai: I'd expect blue to always win inside the B scope
<fantasai> TabAtkins: yellow would win if you have A inside B inside A
<emilio> <A><B><A><X>
<TabAtkins> fantasai: so there's definitely some confusing stuff that happens when taking the outermost
<TabAtkins> fantasai: dunno if we can resolve the confusion
<TabAtkins> (I think it getting yellow in this case is probably the intended behavior, fwiw.)
<emilio> q+
<TabAtkins> fantasai: what behavior they expect is an interesting thing. probably nest step is to have a blog post on the CSSWG blog
<TabAtkins> astearns: miriam says we already know why people nest scopes tho
<TabAtkins> miriam: it would be the similar reason for nesting in general - "i want to drill down"
<TabAtkins> miriam: Often, i've written a scope, but I only want to apply it in a certain place.
<keithamus> q+
<TabAtkins> fantasai: I think it's clear that we'll allow nested scopes. The proximity part is the - what would they use the proximity for rather than just the nested part of the scope
<fantasai> s/we'll allow/people will use/
<astearns> ack emilio
<fantasai> s/nested part/limiting part/
<TabAtkins> emilio: I can't think of any case where you'd nest a scope, but then - that weird situation where you have a lone thing you expect to be inside is outside [ed: i dunno what is being referred to here]
<TabAtkins> emilio: i agree with fantasai that we should reach out about this
<TabAtkins> emilio: I dont' think there's much @scope in the wild, but we could check httparchive
<TabAtkins> emilio: so i don't think i could come up with a use-case that would intentionally break, feels weird to do
<TabAtkins> keithamus: a commont hing i see in preprocessors is to drop in an @include, which might be a whole block of CSS including scoped CSS
<keithamus> ack keithamus
<astearns> ack keithamus
<TabAtkins> keithamus: i have no concept about what that might change, jsut throwing out an example. possibly just accidental that it happens, but it could be a way for nested scopes to happen
<astearns> ack fantasai
<TabAtkins> fantasai: yeah, that's an interesting question. if you have an outer @scope, and you drop rules in, and you expect rules in the scope to defeat things with a weaker proximity
<TabAtkins> fantasai: First block of @scope is 10 proximity, next is 6. Then inside it, we have another rule with an @scope...
<TabAtkins> [i have no idea what this example is]
<TabAtkins> miriam: remember that no block has a score ,it comes from the dom
<noamr> IMO we should leave score around imports to layers rather than add all kinds of default behavior around this
<TabAtkins> fantasai: i dunno i'm confused
<TabAtkins> Agreed, noamr
<stepheckles> q+
<TabAtkins> emilio: so it's not true that the inner scope is always the most relevant for the comparison
<TabAtkins> stepheckles: just thinking thru scenarios where someone would nest
<astearns> ack stepheckles
<TabAtkins> stepheckles: imagine they're nesting just becuase it's now available
<TabAtkins> stepheckles: some authors might miss the part where @scope doesn't add specificity
<TabAtkins> stepheckles: Adding that detail might be useful when asking for details
<TabAtkins> astearns: So let's take this back to the issue and bring it back when we have more clarity/examples
<TabAtkins> miriam: emilio, i didn't catch what you were referring to when you were saying inner scope wasn't alway smost relevant
<TabAtkins> emilio: in the A-B-A-X case
<TabAtkins> miriam: right, that's the point i was making, it's not the innermost *rule*, it's the nearest scope root
<TabAtkins> (Yes, that's the proximity we use in the spec)
noamr commented 2 weeks ago

For the use cases where this applies, can't author use layers? e.g.

Could adding a count of nested @scope work as an approximation that does not require dynamic sizing?

That also seems good to me.

I actually wonder if scope proximity is all that useful to begin with..

Otherwise, we could actually consider removing proximity entirely (as @emilio is hinting at). I kind of regret not making @scope just about scoping.

Proximity is actually the CSS feature we are most excited about.

It is very common for us to have components and layouts with areas that can contain other components. With both potentially having content from a wysiwyg editor. Many CMS's are moving towards very flexible page builders where content editors can nest components in various ways.

The only way to style these as designers would expect them to appear is by using @scope and if @scope has proximity.

An abstracted example of what we often encounter:

(I bet that component authors encounter similar issues but at a more granular/smaller level?)

I really hope that we don't lose proximity.

Can't this be made more explicit with layers representing where the style comes from, rather than with something implicit like proximity?

romainmenke commented 2 weeks ago

Can't this be made more explicit with layers representing where the style comes from, rather than with something implicit like proximity?

How do you mean? Would this take DOM structure into account?

noamr commented 2 weeks ago

Can't this be made more explicit with layers representing where the style comes from, rather than with something implicit like proximity?

How do you mean? Would this take DOM structure into account?

Sorry, I misunderstood the use case at first, please disregard.

mirisuzanne commented 2 weeks ago

The use-case mentioned on the call is:

@scope (a) {
  @scope (b) {
    c { color: blue; }
  }

  color: yellow;
}

With the DOM:

<a><b><a><c>hello</c></a></b></a>

Currently all three proposals above would give the same result, since the proximity of the first step provides a clear winner (a->c is fewer steps than b->c). If we prioritize number of scopes above proximity, we would reverse the result. But at that point we're just re-creating a more blunt form of specificity, right? We're saying that the number or selectors involved should matter more than their proximity in the dom. Which is why we opted for proximity-after-specificity in the first place. And it seems to me (maybe this is what @5t3ph was getting at) that the real concern here is with scopes not adding specificity.

The reason we would expect blue to win, is because we expect specificity.

So @noamr I don't know if you were confused, but I agree with the statement. I don't know that the problem here is how we've defined proximity - but the fact some people might want scopes to increase specificity along the way (which they can do by using &). Or, as you suggest, could use layers. Because the confusion isn't about proximity at all, but wanting to override it sometimes.

noamr commented 2 weeks ago

So @noamr I don't know if you were confused, but I agree with the statement. I don't know that the problem here is how we've defined proximity - but the fact some people might want scopes to increase specificity along the way (which they can do by using &). Or, as you suggest, could use layers. Because the confusion isn't about proximity at all, but wanting to override it sometimes.

I meant that layers don't solve the use case presented in the codepen. But I'm not sure what does exactly and what's the right way to approach it; I would suggest to try to look at the use case of compoents-in-components more holistically rather than jump to a solution that overloads specificity with yet another implicit rule.

dshin-moz commented 2 weeks ago

But at that point we're just re-creating a more blunt form of specificity, right?

Hm, there is a divergence, though? As in, more specific scope selectors don't necessarily equate to "more deeply nested?" e.g. @scope (.foo.bar.baz) { .target { /*...*/ } } versus @scope(.a) { @scope (.b) { .target { /*...*/ } } }

Also, we're firmly in the realm of edge cases, but if there's an implicit selector in the nesting, what's the proposed specificity? e.g. @scope (.a) { @scope (.b) { @scope { .target { /*...*/ } } } }

mirisuzanne commented 2 weeks ago

Hm, there is a divergence, though? As in, more specific scope selectors don't necessarily equate to "more deeply nested?" e.g. @scope (.foo.bar.baz) { .target { /*...*/ } } versus @scope(.a) { @scope (.b) { .target { /*...*/ } } }

They're not identical. But it's a similar heuristic to approximate the narrowness of the selector targeting, rather than anything more nuanced about the DOM structure (which we can't currently do).

Also, we're firmly in the realm of edge cases, but if there's an implicit selector in the nesting, what's the proposed specificity? e.g. @scope (.a) { @scope (.b) { @scope { .target { /*...*/ } } } }

Currently scope roots have no impact on specificity, so .target gives us a specificity of [0,1,0]. The implicit selector there only works in a DOM-nested context, and doesn't impact the specificity calculation.