You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[selectors-4] reconsider specificity rule for :matches() #1027
The specificity of a :matches() pseudo-class is replaced by the specificity of its selector list argument. (The full selector’s specificity is equivalent to expanding out all the combinations in full, without :matches().)
I just realized two things:
this isn't worded in a particularly clear way
I'm actually someone nervous about the performance implications of the only logical way to make it clearer.
What is unclear, I think, is that the concept of specificity of a selector list requires both a selector list and an element being matched, since it uses the highest specificity form that matches the element. The algorithm doesn't make it clear that the element being matched is passed to the algorithm.
But that, in turn, pointed out to me that since :matches() can appear anywhere in a selector (between combinators), there might be multiple elements that could match a given matches. In order to not expose the order in which an implementation searches the subtree to find the set of elements that match the combinators, the specification also needs to require that the specificity of a complex selector be the highest-specificity way of matching that complex selector. This is because the new rule for :matches() introduces the possibility that different ways of matching a complex selector (i.e., pairings between elements and the compound selectors in the complex selector) have different specificity.
This, in turn, requires changes to how implementations match combinators, so that they search the tree for the highest-specificity way. I believe this may have substantive performance implications for at least some combinations of combinators, although I haven't actually worked the problem through yet. Somebody should!
This, in turn, makes me wonder whether we should reconsider whether the new specificity rule for :matches() is actually a good idea. (Another option is marking it at risk, but that has its own problems.)
If we do keep it, I'd like to ensure we add tests that exercise the performance-sensitive cases.
changed the title [-][selectors-44] reconsider specificity rule for :matches()[/-][+][selectors-4] reconsider specificity rule for :matches()[/+]on Feb 11, 2017
the new rule for :matches() introduces the possibility that different ways of matching a complex selector (i.e., pairings between elements and the compound selectors in the complex selector) have different specificity
@dbaron, could you please provide an example of this possibility?
My understanding so far is that :matches() is basically just kind of syntactic sugar for shortening long lists of selectors that have common parts. So, when a browser checks an element against something like .menu > :matches(a, .current) > .icon, under the hood it does the same work as for checking the same element against .menu > a > .icon, .menu > .current > .icon. Yes, it can possibly introduce performance issues for complex selectors with several :matches(), when the browser would have to check the element against all the combinations of their arguments (so the numbers of the variants would multiply), but it still would be in fact just a very long list of the usual selectors, at least one of which is the most specific. Am I wrong?
Also, given that :matches() has already been implemented in WebKit for about a year and a half, maybe they can provide some feedback about how they dealt with these issues?
Yes, :matches() is solely syntactic sugar (tho with complex selector arguments, the expanded form can get very verbose - try to expand out :matches(.a .b .c, .d .e .f) fully and correctly).
Yes, :matches() is solely syntactic sugar (tho with complex selector arguments, the expanded form can get very verbose - try to expand out :matches(.a .b .c, .d .e .f) fully and correctly).
This example isn't very verbose. Consider .a .b .c :matches(.d .e .f), which can be very verbose.
could you please provide an example of this possibility?
If I understand correctly, an example could be
<divid="a"></div><divid="b"><divid="c"></div><divid="d" class="foo"><spanid="e">Styles are being assigned to this element</span></div></div>
:matches(#a,*) +:matches(.foo,*) span
An implementation could detect that
#e matches span with specificity (0,0,1).
#d matches both .foo and *, so it matches :matches(.foo, *) with specificity (0,1,0).
#c matches * but not #a, so it matches :matches(#a, *) with specificity (0,0,0).
So the whole selector is matched with specificity (0,0,0)+(0,1,0)+(0,0,1) = (0,1,1)
However, there is another way to match the selector:
#e matches span with specificity (0,0,1).
#b matches * but not .foo, so it matches :matches(.foo, *) with specificity (0,0,0).
#a matches both *#a and *, so it matches :matches(#a, *) with specificity (1,0,0).
So the whole selector is matched with specificity (1,0,0)+(0,0,0)+(0,0,1) = (1,0,1)
It would be bad if different implementations calculated the specificity of :matches on different elements, because this could affect the total specificity. :nth-* are also affected:
the specificity of an :nth-child(), :nth-last-child(), :nth-of-type(), or :nth-last-of-type() selector is the specificity of the pseudo class itself (counting as one pseudo-class selector) plus the specificity of its selector list argument (if any)
I see various possible solutions:
Properly specify on which elements the pseudo-classes are calculated, e.g. the ones that maximize the total specificity. This might have performance problems.
Ignore the selector list argument and let the specificity be (0,1,0) like a normal pseudo-class. :-moz-any and :-webkit-any seem to behave like this.
Remove :something/:is and define the specificity of :matches to be (0,0,0).
and by the rules of the selectors list specificity, its specificity will be the largest one of the individual selectors in the list that match the element, in this example it would be (1, 0, 1) of #a + * span. There is only one way to expand :matches() into selector list, and any selector list has only one maximum specificity value of its parts (even if several parts have the same specificity), so there is no place for the ambiguity to occur.
Yes, with that interpretation the specificity is not ambiguous, but as dbaron said, the definition "isn't worded in a particularly clear way". And may not be the best choice if it has performance problems (expanding :matches can produce exponentially-long selectors).
Attempted to address the clarity issue in 88b91c0; the specificity rules were written with only compound selectors in mind, and so for complex arguments the existing prose indeed didn't make sense. I don't think it's perfect now, but should be better.
That doesn't address the perf concerns, though. @Loirooriol's comment #1027 (comment) summarizes some of the options; I'll repeat them here and add the missing one:
Specificity of :matches() is the specificity of the most specific selector that can match. This is what the spec says now, and is the only logical conclusion of treating :matches() as syntactic sugar to the extent that it can be (i.e. when it contains compound selectors only).
Specificity of :matches() is the specificity of any other pseudo-class. This is in conflict with how :not() works already, and also means that S:nth-child(n) and :nth-child(n of S) have different specificities, which is imho not reasonable. (They are functionally different, but have the same weight of meaning.)
Specificity of :matches() is zero. Same problems as (0,1,0).
Specificity of :matches() is that of its most specific argument. We lose the equivalency of :matches(a,b,c) and a,b,c, but we maintain consistency with :not(), and S:nth-child(n) & :nth-child(n of S) maintain equivalent specificities.
The last option looks promising because the specificity of the selector can be determined without expanding it, but would it play a significant role in practice? If checking for selector matching would require expanding it anyway, will calculating the specificity dynamically necessary need any overhead in terms of performance?
Maybe @victoriasu can provide some details from the implementer's perspective?
@fantasai While definitely useful, I think 88b91c0 still does not clarify that the "calculating a selector’s specificity" algorithm requires an element, and which element is used for the argument of :matches and friends. But I guess this can wait until this issue is resolved.
And thanks for adding the missing option, I thought I included it but it seems I forgot. It may be the nicest one. I'm not an expert but I expect that matching :matches without expanding it should be feasible, @SelenIT. Otherwise, I think it should be confined to the snapshot profile.
[:matches(.a .b .c, .d .e .f)] isn't very verbose. Consider .a .b .c :matches(.d .e .f), which can be very verbose.
Oh shoot, you're right, I meant to write :matches(.a, .b, .c):matches(.d, .e, .f), not a single comma-separated one. (As written, my example is equivalent to a plain .a .b .c, .d .e .f selector list.)
As far as I understand, we have 2 implementations of :matches() per the existing spec (option 1 from the above list) since yesterday, one shipped and one experimental behind the flag. (Well done, @victoriasu!)
Is the way of calculating its specificity still subject to change?
Anyone here want to take a stab at reviewing it to make sure a) I got it right b) I fixed all the points that needed fixing c) It's sufficiently clear? :)
:not has a different text than :matches and :nth-child, this may make people think their specificity is not calculated in the same way. Since the point is that the element doesn't matter, I would just use "whenever it matches any element" in all three cases.
I would also prefer if the specificity was defined with a properly clear algorithm. The current one can be ambiguous, e.g. in
<li>count the number of ID selectors in the selector (= <var>A</var>)
people may think ID selectors inside :matches should also be counted, but it's only the "top-level" ones.
Something like this:
Given a selector s and an element e which is matched by s, the specificity of s in e is calculated as follows:
If s is a complex selector, return the absolute specificity of s.
Else, s is a selector list. Return the maximum among the absolute specificities of the selectors in the list that match e.
The absolute specificity of a selector s is calculated as follows:
If s is an universal selector, or a :where() pseudo-class selector, return (0,0,0).
If s is a type selector or a pseudo-element, return (0,0,1).
If s is a :matches(), :not(), or :has() pseudo-class selector, return the absolute specificity of its selector list argument.
If s is a :nth-child(), or :nth-last-child() pseudo-class selector, return (0,1,0) plus the absolute specificity of its selector list argument (if any).
If s is a class, attribute, or pseudo-class selector, return (0,1,0).
If s is an ID selector, return (1,0,0).
If s is a compound selector, return the sum of the absolute specificities of the simple selectors in s.
If s is a complex selector, return the sum of the absolute specificities of the compound selectors in s.
Else, s is a selector list. Return the maximum among the absolute specificities of the selectors in the list.
Doesn't the phrase "its most specific argument" (implying that this functional pseudo-class takes several arguments) technically contradict the phrase "a functional pseudo-class taking a selector list as its argument" from the definition (implying that the selector list as a whole is considered a single argument)? Maybe it would be better to reuse the phrase "the most specific complex selector in its selector list argument" in the definition of :matches(), for consistency with the definition of :nth-*-child(... of S) and with the section about the specificity calculation?
Oh dang, actually, I think I remember there was an earlier intent that we could use :matches() as an end-run around the terrible "a syntax error in one complex selector kills the whole sequence" behavior that Selectors is stuck with. As such, it should be clarified to take an <any-value>, then split on top-level commas, then parse each result as a <complex-selector> and just ignore invalid ones.
@tabatkins, should :matches()/:is() be the only selector with such behavior, or the idea is worth generalizing to all functional pseudo-classes whose argument can contain a selector list (:not(), :nth-/:nth-last-child(... of S), :has(), probably where(), maybe time-dimensional pseudo-classes)?
OK, I'm closing out this issue. @Loirooriol I didn't remove the specific examples in favor of "any element" because I think they help point out the unintuitive result that while em, #foo matching <em> has a specificity of 1 tag selector, :is(em, #foo) matching <em> has the specificity of 1 ID selector. Also, rewriting the whole section should have its own issue. :)
I am wondering now if we should have been choosing the least specific argument instead of the most specific one, though...
Activity
[-][selectors-44] reconsider specificity rule for :matches()[/-][+][selectors-4] reconsider specificity rule for :matches()[/+]SelenIT commentedon Apr 5, 2017
@dbaron, could you please provide an example of this possibility?
My understanding so far is that
:matches()is basically just kind of syntactic sugar for shortening long lists of selectors that have common parts. So, when a browser checks an element against something like.menu > :matches(a, .current) > .icon, under the hood it does the same work as for checking the same element against.menu > a > .icon, .menu > .current > .icon. Yes, it can possibly introduce performance issues for complex selectors with several:matches(), when the browser would have to check the element against all the combinations of their arguments (so the numbers of the variants would multiply), but it still would be in fact just a very long list of the usual selectors, at least one of which is the most specific. Am I wrong?Also, given that
:matches()has already been implemented in WebKit for about a year and a half, maybe they can provide some feedback about how they dealt with these issues?tabatkins commentedon Apr 5, 2017
Yes, :matches() is solely syntactic sugar (tho with complex selector arguments, the expanded form can get very verbose - try to expand out
:matches(.a .b .c, .d .e .f)fully and correctly).upsuper commentedon Dec 21, 2017
This example isn't very verbose. Consider
.a .b .c :matches(.d .e .f), which can be very verbose.Loirooriol commentedon Dec 31, 2017
@SelenIT
If I understand correctly, an example could be
An implementation could detect that
#ematchesspanwith specificity (0,0,1).#dmatches both.fooand*, so it matches:matches(.foo, *)with specificity (0,1,0).#cmatches*but not#a, so it matches:matches(#a, *)with specificity (0,0,0).So the whole selector is matched with specificity (0,0,0)+(0,1,0)+(0,0,1) = (0,1,1)
However, there is another way to match the selector:
#ematchesspanwith specificity (0,0,1).#bmatches*but not.foo, so it matches:matches(.foo, *)with specificity (0,0,0).#amatches both*#aand*, so it matches:matches(#a, *)with specificity (1,0,0).So the whole selector is matched with specificity (1,0,0)+(0,0,0)+(0,0,1) = (1,0,1)
It would be bad if different implementations calculated the specificity of
:matcheson different elements, because this could affect the total specificity.:nth-*are also affected:I see various possible solutions:
:-moz-anyand:-webkit-anyseem to behave like this.:something/:isand define the specificity of:matchesto be (0,0,0).SelenIT commentedon Dec 31, 2017
@Loirooriol, I don’t see this ambiguity in the spec. Since the spec for
:matches()specificity saysthis example will be expanded as
#a + .foo span, #a + * span, * + .foo span, * + * span { ... }and by the rules of the selectors list specificity, its specificity will be the largest one of the individual selectors in the list that match the element, in this example it would be (1, 0, 1) of
#a + * span. There is only one way to expand:matches()into selector list, and any selector list has only one maximum specificity value of its parts (even if several parts have the same specificity), so there is no place for the ambiguity to occur.Do I miss something?
Loirooriol commentedon Dec 31, 2017
Yes, with that interpretation the specificity is not ambiguous, but as dbaron said, the definition "isn't worded in a particularly clear way". And may not be the best choice if it has performance problems (expanding
:matchescan produce exponentially-long selectors).fantasai commentedon Jan 1, 2018
Attempted to address the clarity issue in 88b91c0; the specificity rules were written with only compound selectors in mind, and so for complex arguments the existing prose indeed didn't make sense. I don't think it's perfect now, but should be better.
That doesn't address the perf concerns, though. @Loirooriol's comment #1027 (comment) summarizes some of the options; I'll repeat them here and add the missing one:
:matches()is the specificity of the most specific selector that can match. This is what the spec says now, and is the only logical conclusion of treating:matches()as syntactic sugar to the extent that it can be (i.e. when it contains compound selectors only).:matches()is the specificity of any other pseudo-class. This is in conflict with how:not()works already, and also means thatS:nth-child(n)and:nth-child(n of S)have different specificities, which is imho not reasonable. (They are functionally different, but have the same weight of meaning.):matches()is zero. Same problems as (0,1,0).:matches()is that of its most specific argument. We lose the equivalency of:matches(a,b,c)anda,b,c, but we maintain consistency with:not(), andS:nth-child(n)&:nth-child(n of S)maintain equivalent specificities.SelenIT commentedon Jan 2, 2018
The last option looks promising because the specificity of the selector can be determined without expanding it, but would it play a significant role in practice? If checking for selector matching would require expanding it anyway, will calculating the specificity dynamically necessary need any overhead in terms of performance?
Maybe @victoriasu can provide some details from the implementer's perspective?
Loirooriol commentedon Jan 2, 2018
@fantasai While definitely useful, I think 88b91c0 still does not clarify that the "calculating a selector’s specificity" algorithm requires an element, and which element is used for the argument of
:matchesand friends. But I guess this can wait until this issue is resolved.And thanks for adding the missing option, I thought I included it but it seems I forgot. It may be the nicest one. I'm not an expert but I expect that matching
:matcheswithout expanding it should be feasible, @SelenIT. Otherwise, I think it should be confined to the snapshot profile.victoriasu commentedon Jan 15, 2018
@SelenIT I am planning on expanding
:matchesfor the specificity. Webkit appears to also use the expanded specificity from what I see when running: https://jsfiddle.net/victoriaytsu/vwsfsfr6/tabatkins commentedon Jan 16, 2018
Oh shoot, you're right, I meant to write
:matches(.a, .b, .c):matches(.d, .e, .f), not a single comma-separated one. (As written, my example is equivalent to a plain.a .b .c, .d .e .fselector list.)SelenIT commentedon Feb 6, 2018
As far as I understand, we have 2 implementations of
:matches()per the existing spec (option 1 from the above list) since yesterday, one shipped and one experimental behind the flag. (Well done, @victoriasu!)Is the way of calculating its specificity still subject to change?
Loirooriol commentedon Feb 7, 2018
So at first glance at the Chromium's code it seems that very few expansions are allowed:
https://chromium-review.googlesource.com/c/chromium/src/+/879982/15/third_party/WebKit/Source/core/css/CSSSelectorList.cpp#165
Effectively https://jsfiddle.net/u9q1ogc7/ demonstrates the failure. Text should be green.
I think
:matchesmatching should not use expansion and the specificity should be fixed.12 remaining items
Loirooriol commentedon May 30, 2018
Don't forget about
:nth-childand:nth-last-child.[selectors-4] Make specificity of :matches(), :has(), and :nth-child(…
fantasai commentedon Oct 27, 2018
OK, committed these changes to the ED:
https://drafts.csswg.org/selectors-4/
76eefbb
Anyone here want to take a stab at reviewing it to make sure a) I got it right b) I fixed all the points that needed fixing c) It's sufficiently clear? :)
ewilligers commentedon Oct 27, 2018
Looks good.
Loirooriol commentedon Oct 28, 2018
csswg-drafts/selectors-4/Overview.bs
Line 1124 in 76eefbb
Typo: extra
]in[hidden]]csswg-drafts/selectors-4/Overview.bs
Line 3317 in 76eefbb
Typo:
'':matches''should be'':matches()'', otherwise it's not linked.csswg-drafts/selectors-4/Overview.bs
Line 3319 in 76eefbb
Suggestion: link "complex selector" to https://drafts.csswg.org/selectors-4/#complex
csswg-drafts/selectors-4/Overview.bs
Lines 3336 to 3337 in 76eefbb
csswg-drafts/selectors-4/Overview.bs
Lines 3347 to 3348 in 76eefbb
csswg-drafts/selectors-4/Overview.bs
Line 3352 in 76eefbb
:nothas a different text than:matchesand:nth-child, this may make people think their specificity is not calculated in the same way. Since the point is that the element doesn't matter, I would just use "whenever it matches any element" in all three cases.I would also prefer if the specificity was defined with a properly clear algorithm. The current one can be ambiguous, e.g. in
csswg-drafts/selectors-4/Overview.bs
Line 3301 in 76eefbb
people may think ID selectors inside
:matchesshould also be counted, but it's only the "top-level" ones.Something like this:
SelenIT commentedon Oct 29, 2018
Doesn't the phrase "its most specific argument" (implying that this functional pseudo-class takes several arguments) technically contradict the phrase "a functional pseudo-class taking a selector list as its argument" from the definition (implying that the selector list as a whole is considered a single argument)? Maybe it would be better to reuse the phrase "the most specific complex selector in its selector list argument" in the definition of
:matches(), for consistency with the definition of:nth-*-child(... of S)and with the section about the specificity calculation?tabatkins commentedon Oct 29, 2018
Oh dang, actually, I think I remember there was an earlier intent that we could use
:matches()as an end-run around the terrible "a syntax error in one complex selector kills the whole sequence" behavior that Selectors is stuck with. As such, it should be clarified to take an<any-value>, then split on top-level commas, then parse each result as a<complex-selector>and just ignore invalid ones.I'll open a different issue about this.
SelenIT commentedon Oct 29, 2018
@tabatkins, should
:matches()/:is()be the only selector with such behavior, or the idea is worth generalizing to all functional pseudo-classes whose argument can contain a selector list (:not(),:nth-/:nth-last-child(... of S),:has(), probablywhere(), maybe time-dimensional pseudo-classes)?[selectors-4] Minor fixes to specificity examples and fixing typos. #…
fantasai commentedon Nov 15, 2018
OK, I'm closing out this issue. @Loirooriol I didn't remove the specific examples in favor of "any element" because I think they help point out the unintuitive result that while
em, #foomatching<em>has a specificity of 1 tag selector,:is(em, #foo)matching<em>has the specificity of 1 ID selector. Also, rewriting the whole section should have its own issue. :)I am wondering now if we should have been choosing the least specific argument instead of the most specific one, though...