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-view-transitions-2] Specificity of view transition pseudos with classes #9887

Closed
vmpstr opened this issue Feb 1, 2024 · 19 comments · Fixed by #9941
Closed

[css-view-transitions-2] Specificity of view transition pseudos with classes #9887

vmpstr opened this issue Feb 1, 2024 · 19 comments · Fixed by #9941
Labels
css-view-transitions-1 View Transitions; Bugs only css-view-transitions-2 View Transitions; New feature requests Needs Edits

Comments

@vmpstr
Copy link
Member

vmpstr commented Feb 1, 2024

in #8319 we've added view-transition-class that reflects in the view transition pseudo tree, so things like the following become valid selectors:

/* 1 */ ::view-transition-group(foo) /* foo view transition name with any classes */
/* 2 */ ::view-transition-image-pair(*.bar) /* any view transition name with a "bar" class */
/* 3 */ ::view-transition-old(foo.bar) /* foo view transition name with at least class "bar" */
/* 4 */ ::view-transition-new(foo.bar.baz) /* foo view transition name with at least "bar" and "baz" classes */

The question here is which specificity should these view transition elements have. Some points to consider:

  • It feels like 1 should have higher specificity than 2 (with any number of classes)
  • None of these should rise to the level of id-like specificity, since it's still just a pseudo element
  • More classes feels more specific than fewer classes

One off-the-top of the head proposal is something like:
class-like-specificity * (5 * id-is-present + number-of-classes)

meaning that the following holds:

  • 1 has specificity 5 class-like
  • 2 has specificity 1 class-like
  • 3 has specificity 6 class-like
  • 4 has specificity 7 class-like
  • ::view-transition-group(*) remains 0

The worry of course is that 5 classes is as specific as 1 name. We could make the constant higher like 10 or something, but there's also a worry that ::view-transition-group(foo) would become 10 class-like specificity which is pretty high if we combine this with other class like selectors. For example:

::view-transition-group(foo)

would be higher specificity (I think) than

.foo.bar::view-transition-group(*)

which also seems wrong

We can have our own specificity ordering within the argument, but how it maps to outside of the argument is unclear

/cc @tabatkins @fantasai for question whether there's prior art here

@vmpstr vmpstr added the css-view-transitions-2 View Transitions; New feature requests label Feb 1, 2024
@noamr
Copy link
Collaborator

noamr commented Feb 2, 2024

If we defined that ::view-transition-group(a) has an ID-level specificity of 1, and ::view-transition-group(*.cls.bar) has have a class-level specificity of 2, etc, makes the most sense to me. Since these are pseudo-elements they don't compete on the same ID space as regular elements anyway (or at least not in a way I know how to reproduce in a WPT...)

@romainmenke
Copy link
Member

I am a bit concerned that authors will have a hard time understanding this if this has different rules and weights than regular selectors for things that look similar to regular class, type selectors

Specificity is already one of the least well understood parts of CSS.

@noamr
Copy link
Collaborator

noamr commented Feb 2, 2024

I am a bit concerned that authors will have a hard time understanding this if this has different rules and weights than regular selectors for things that look similar to regular class, type selectors

Specificity is already one of the least well understood parts of CSS.

They look similar to type selectors just because we didn't add a # (perhaps we should have), but for all intents and practical purposes view-transition-names are more similar to IDs than to tag names. (But I appreciate the concern)

@romainmenke
Copy link
Member

They look similar to type selectors just because we didn't add a # (perhaps we should have), but for all intents and practical purposes view-transition-names are more similar to IDs than to tag names

Is it too late to change that? :)

@noamr
Copy link
Collaborator

noamr commented Feb 2, 2024

Sorry, my earlier comment was wrong. There is an actual conflict between IDs in regular space and pseudo space.

@vmpstr
Copy link
Member Author

vmpstr commented Feb 2, 2024

I think the confusion here, at least it was for me initially, is that we're not talking about the specificity of the argument inside ::view-transition-* pseudos, but rather the specificity of the pseudo itself given different types of arguments. So it does play in the regular space.

I agree that in the pseudo space, the solution is pretty simple: id is id-like and each class is class-like. When bringing that to the regular space, there needs to be some mapping. One idea that came up is to maybe introduce another tie-breaker category of specificity. That is, instead of id-like, class-like, type-like have id-like, class-like, type-like, argument-tie-breaker-like in that order. That way, all of the specificity ordering is somewhat independent of the argument, unless there's a tie. In case of a tie, we look at the argument

@tabatkins
Copy link
Member

Why shouldn't the specificity just be reflected straight into normal specificity, as ID and class values? These are, by definition, not fighting with any non-VT styles.

The only concern, I guess would be that it would mean div::view-transition-group(foo) would beat .foo::view-transition-group(.bar), since it would be [1,0,1] vs [0,2,0]. Is that good, or bad? Switching it to a new, lowest specificity term would mean the .foo rule wins, and always will regardless of argument. Is either option very likely to be the right choice, or is it going to be a contextual decision always?

Is there any potential for other functions we might want to give a weak, tie-breakery notion of specificity to?

@vmpstr
Copy link
Member Author

vmpstr commented Feb 5, 2024

The only concern, I guess would be that it would mean div::view-transition-group(foo) would beat .foo::view-transition-group(.bar)

Yeah, that's the only reason why reflecting straight into normal specificity is questionable.

Is either option very likely to be the right choice, or is it going to be a contextual decision always?

This is something that I'm struggling to answer. Personally, it makes far more sense for div::view-transition-group(...) to be lower specificity than .foo::view-transition-group(...) regardless of what ... actually is. That is, in my mind, the selector outside of the view transition argument is a stronger specificity indicator. Once those are resolved, and if there's a tie, then look at the argument.

I can't picture a scenario where that should not be the case, but that's not to say that those cases don't exist.

@noamr
Copy link
Collaborator

noamr commented Feb 5, 2024

The only thing that's clear to me is that when the external selector has a tie, the internal one should work, as in ::view-transition-group(*.foo) should be stronger than ::view-transition-group(*), and ::view-transition-group(bar) should be stronger than both.

In terms of mixed-spaces, envision the following css;

html:has(.loaded-hero)::view-transition-group(*) { animation-duration: 1s; }
html::view-transition-group(*.box) { animation-duration: 100ms; }

I'm not sure I'd expect from seeing this that the duration should be 1s because normal classes receive precedence over vt classes. And I'm not sure we can find any solution that would meet expectations in a self-explanatory way TBH. So the best we can probably do is be consistent with the existing sometimes-surprising behavior and just pick something, probably vt-name maps to ID and vt-class maps to class, and recommend that people use layers if they need things to be different.

@tabatkins
Copy link
Member

Yeah, that's my concern here - sometimes, the non-VT-pseudo part of the selector is gonna be the more meaningful part and should be paid attention to regardless of the VT argument, and sometimes the opposite will be true, and I'm not sure either is sufficiently obvious as a default.

So yeah, while my first suggestion would be to just give the VT pseudo a standard, flat specificity regardless of argument, if we do want tie-breaking to still occur, then just letting the names and classes contribute to normal specificity seems reasonable here, since there are various ways for authors to control this as they need.

@bramus
Copy link
Contributor

bramus commented Feb 5, 2024

Time for a new specificity category?

  • D = the specificity of the inner “selectors” of the pseudo-element

That way:

  • div::view-transition-group(foo) = [0, 0, 1, [1, 0, 0]]
  • ::view-transition-image-pair(*.bar) = [0, 0, 0, [0, 1, 0]]
  • .foo::view-transition-group(.bar) = [0, 1, 0, [0, 1, 0]]

(Oh, did I just nest specificity? 🙈)

@tabatkins
Copy link
Member

Yeah, that was Vlad's idea. However, that presupposes the answer to my question - it implies that the non-VT part of the selector should always win if it can, and we only look at the VT part when there's a tie.

@vmpstr
Copy link
Member Author

vmpstr commented Feb 6, 2024

Discussed this internally and could not reach a clear consensus. In the spirit of "arguing for the other side", I found the following arguments in support of parameter directly affecting specificity of the selector:

From https://drafts.csswg.org/selectors/#matches-pseudo

NOTE: The specificity of the :is() pseudo-class is replaced by the specificity of its most specific argument. Thus, a selector written with :is() does not necessarily have equivalent specificity to the equivalent selector written without :is() For example, if we have :is(ul, ol, .list) > [hidden] and ul > [hidden], ol > [hidden], .list > [hidden] a [hidden] child of an ol matches the first selector with a specificity of (0,2,0) whereas it matches the second selector with a specificity of (0,1,1). See § 17 Calculating a selector’s specificity.

From: https://drafts.csswg.org/css-scoping/#host-selector

The specificity of :host is that of a pseudo-class. The specificity of :host() is that of a pseudo-class, plus the specificity of its argument.

NOTE: This is different from the specificity of similar pseudo-classes, like :is() or :not(), which only take the specificity of their argument. This is because :host is affirmatively selecting an element all by itself, like a "normal" pseudo-class; it takes a selector argument for syntactic reasons (we can’t say that :host.foo matches but .foo doesn’t), but is otherwise identical to just using :host followed by a selector.

From https://drafts.csswg.org/css-scoping/#slotted-pseudo

The specificity of ::slotted() is that of a pseudo-element, plus the specificity of its argument.

From https://drafts.csswg.org/selectors/#nth-child-pseudo

NOTE: The specificity of the :nth-child() pseudo-class is the specificity of a single pseudo-class plus, if S is specified, the specificity of the most specific complex selector in S. See § 17 Calculating a selector’s specificity. Thus S:nth-child(An+B) and :nth-child(An+B of S) have the exact same specificity, although they do differ in behavior (see example below).

However, specificity of ::part seems to be 0, regardless of the argument? (I couldn't find anything on ::part's specificity, other than what Chromium implements)

More importantly there is no prior attempts at the tie-breaker specificity, whether or not it may make sense for some of the selectors above.

One thing to note is that most of these selectors above are "structural" in that their arguments make sense outside of the argument as well. For example, the parameter in :has(#foo), namely #foo has a meaning as a selector on its own. For ::view-transition-* elements, the selector is not structural in that we are indeed selecting the ::view-transition-* pseudo element, qualified by its parameter. The parameter is a property copied from the source element, either view-transition-name or view-transition-class or both, which on their own are not valid selectors and thus don't have a specificity. We're giving a specificity to them to mean "id" and "class" since that's how they are meant to be used. This is likely a minor point.

@noamr
Copy link
Collaborator

noamr commented Feb 6, 2024

Summarizing the internal sync, there was no clear consensus. There seems to be advantages to adding new specificity levels for the pseudo-element's internal argument, but it's unclear if the advantages of that outweigh the cost and whether this would adhere better to developer expectations.

Proposing to resolve between the two options at the CSSWG (F2F?).

@argyleink
Copy link
Contributor

What if things are flatter and all worth 1 (except for *)?

/* 1 */ ::view-transition-group(foo) /* foo view transition name with any classes */
/* 2 */ ::view-transition-image-pair(*.bar) /* any view transition name with a "bar" class */
/* 3 */ ::view-transition-old(foo.bar) /* foo view transition name with at least class "bar" */
/* 4 */ ::view-transition-new(foo.bar.baz) /* foo view transition name with at least "bar" and "baz" classes */
  • 1 has specificity 1 (it's weighted as 1 but known as strong filter since it calls out a single item)
  • 2 has specificity 1
  • 3 has specificity 2
  • 4 has specificity 3
  • ::view-transition-group(*) remains 0

This would make the author experience less about specificity and more about being specific about the targets. Targeting a pseudo by name already reduces the targets of the selector by a lot, so I don't think it needs higher specificity when it's already so scoped. I see an issue being between #1 and #2 though in that order in the stylesheet would be needed to resolve the final styles. So, i think authors could adjust the selector to be more like 3 or 4, compounding the class with the VT name to "win" and get the end style they want, and thus not needing to worry about rule order. The power is in their hands without needing to learn a new scoring edge case dealing with VT names/IDs. Lean into compound selectors instead of a new specificity system?

@noamr
Copy link
Collaborator

noamr commented Feb 6, 2024

What if things are flatter and all worth 1 (except for *)?

This doesn't answer the question of "1 what"? Is it equivalent to 1 class/type/id/something new?

/* 1 */ ::view-transition-group(foo) /* foo view transition name with any classes */
/* 2 */ ::view-transition-image-pair(*.bar) /* any view transition name with a "bar" class */
/* 3 */ ::view-transition-old(foo.bar) /* foo view transition name with at least class "bar" */
/* 4 */ ::view-transition-new(foo.bar.baz) /* foo view transition name with at least "bar" and "baz" classes */
  • 1 has specificity 1 (it's weighted as 1 but known as strong filter since it calls out a single item)

I don't understand why this is a "strong filter". It means that if you put (1) and (2) they would resolve in document order.

@bramus
Copy link
Contributor

bramus commented Feb 6, 2024

What if things are flatter and all worth 1 (except for *)?

This doesn't answer the question of "1 what"? Is it equivalent to 1 class/type/id/something new?

I discussed this with Adam earlier today and it would be 1 type.

I think it could actually work:

  • No compat issue with what is currently shipping
    • The ident used in a vt-pseudo now does not suddenly become more specific than a class-on-root selector with no ident
  • It’s not complex: just cound the parts in vt‘s compound. The more you count, the more specific you are in selecting the right snapshot.
  • Good separation between selecting the root and selecting the snapshots (the one doesn’t really influence the other)

@noamr
Copy link
Collaborator

noamr commented Feb 6, 2024

What if things are flatter and all worth 1 (except for *)?

This doesn't answer the question of "1 what"? Is it equivalent to 1 class/type/id/something new?

I discussed this with Adam earlier today and it would be 1 type.

I think it could actually work:

  • No compat issue with what is currently shipping

    • The ident used in a vt-pseudo now does not suddenly become more specific than a class-on-root selector with no ident
  • It’s not complex: just cound the parts in vt‘s compound. The more you count, the more specific you are in selecting the right snapshot.

  • Good separation between selecting the root and selecting the snapshots (the one doesn’t really influence the other)

OK interesting proposal! A bit hard for me to come to terms with ::view-transition-group(*.class) having the same speficifity as ::view-transition-group(name) but perhaps that's ok

@noamr noamr added the css-view-transitions-1 View Transitions; Bugs only label Feb 7, 2024
@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [css-view-transitions-2] Specificity of view transition pseudos with classes, and agreed to the following:

  • RESOLVED: for view transition pseudos, have * argument as 0 specificity, and everything else 1 type like specificity
The full IRC log of that discussion <TabAtkins> vmpstr: We have VT pseudos that take a paramter
<TabAtkins> vmpstr: take name, *, and now classes
<TabAtkins> vmpstr: makes the arg syntax more involved
<TabAtkins> vmpstr: before classes, specificifity of the pseudo was 0 if *, and 1 tag-like specificity if it took a name
<TabAtkins> vmpstr: with classes, should probably change a bit. a few options
<TabAtkins> vmpstr: First option, and front-runner, is have every non-asterisk token count as a tag for specificity. just add them up
<TabAtkins> vmpstr: minor problem here is having a name and a class has the same specificity. only downside.
<TabAtkins> vmpstr: Second option is have the name be ID-like and class be class-like
<TabAtkins> vmpstr: This is also simple, and similar to :has()/:is()
<TabAtkins> vmpstr: counter-argument is :has/is are more structural, this is specifically a VT element
<TabAtkins> vmpstr: Last option is to use the argument just as a tiebreaker
<bramus> q+
<khush> q+
<TabAtkins> vmpstr: so you use the argument solely if selectors entirely tie.
<TabAtkins> vmpstr: a bit more complicated to spec and implement
<TabAtkins> vmpstr: I initially thought the tiebreaker is best, but I'm convinced now it's more complex than necessary. I think option 1 is good enough, where every token is just a tag-like for spec.
<miriam> ack bramus
<TabAtkins> bramus: I'm on board with Vlad - option 3 was my first, but option 1 is easy to comprehend.
<TabAtkins> bramus: I dont' think option 2 is viable since you're changing existing beahvior
<TabAtkins> bramus: often authors today are adding a clause to the root to flip the animation direction
<TabAtkins> bramus: that adds a class-like specificity to make it win
<TabAtkins> bramus: If we change to have the name be ID-like, it could override those. so it's a breaking change
<miriam> ack khush
<TabAtkins> khush: echoing Vlad, if we made an ideal choice we'd add a new specificity term
<fremy> q?
<fremy> q+
<TabAtkins> khush: the pseudo arguments are just what specific pseudo you're selecting, my intuition it's less important than what element is being targeted for the pseudo
<TabAtkins> khush: but it is indeed a lot more complex, and so just doing Option 1 is fine
<TabAtkins> khush: I'm the one that suggested giving the name ID-like spec, but we have already specced different beahvior indeed
<TabAtkins> khush: one nice thing is the another feature for VT types lets you not rely on outside elements to select. hopefully that can reduce the issue with specificity - you'll generally just use the VT type only
<khush> q?
<miriam> ack fremy
<TabAtkins> fremy: Is this even required?
<TabAtkins> fremy: current behavior is just [0,0,0] or [0,0,1]
<TabAtkins> fremy: If you do that you can just order them in your stylesheet as needed
<TabAtkins> fremy: Is there really a use-case for having them implicitly ordered somehow? Just wondering if it's even worth increasing the specificity
<TabAtkins> vmpstr: good question. if you order them correctly in your stylesheet - we still have to say what happens if you specify a class but not a name
<noamr> q+
<TabAtkins> vmpstr: But adding them to get a larger count is optional, sure. If just feels more specific if you add more class names, or class+name, then just adding a name by itself
<TabAtkins> vmpstr: So just adding them up is probably Good Enough and somewhat intuitive
<TabAtkins> fremy: Yeah, just something new to learn. if it's not required, might be okay to just do nothing new
<TabAtkins> vmpstr: So your proposal is just "anything but asterisk is [0,0,1]"
<TabAtkins> noamr: And that doesn't compete with HTML in the selector
<miriam> ack noamr
<TabAtkins> vmpstr: And then your tiebreaker would be declaration order
<TabAtkins> bramus: i'm not sure if having asterisk 0 and anything else be 1...
<TabAtkins> bramus: i can imagine a scenario "all .card animate one way, but one specific card animate another"
<khush> q+
<miriam> ack khush
<TabAtkins> vmpstr: if that specific one is targeted with a name, wouldn't fix things - still would be [0,0,1]
<TabAtkins> khush: yeah intuitively you might write ::vt(special-card) and ::vt(.card), expecting first to win, but that's a harder model to specify
<TabAtkins> khush: So maybe better here indeed to just rely on name ordering
<vmpstr> proposal: for view transition pseudos, have * argument as 0 specificity, and everything else 1 type like specificity
<TabAtkins> miriam: objections?
<TabAtkins> RESOLVED: for view transition pseudos, have * argument as 0 specificity, and everything else 1 type like specificity

noamr added a commit to noamr/csswg-drafts that referenced this issue Feb 15, 2024
khushalsagar pushed a commit that referenced this issue Feb 18, 2024
… with a class (#9941)

* [css-view-transitions-2] Amend specificity of view-transition pseudos with a class

Closes #9887
See [resolution](https://github.com/w3c/csswg-drafts/9887#issuecomment-1939286361)

* Reword specificity

* Fix cruft

* Reframe
romainmenke added a commit to csstools/postcss-plugins that referenced this issue Feb 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
css-view-transitions-1 View Transitions; Bugs only css-view-transitions-2 View Transitions; New feature requests Needs Edits
Projects
Status: Monday morning
8 participants