Pseudo-elements cannot be represented by the matches-any pseudo-class; they are not valid within :matches().
https://www.w3.org/TR/2018/WD-selectors-4-20180201/#matches
Safari support :matches(::before)
I guess it can be useful in something#very.long :matches(::before, ::after)
. But these pseudo-elements don't have internal structure and thus can't be followed by a combinator, so maybe a :matches
that contains such pseudo-elements should neither.
The reason is that pseudo-elements, semantically, contain a combinator in them - if you start with .foo
, extending to .foo.bar
gives you a subset of the same elements, while extending to .foo::bar
gives you something completely different, like .foo > .bar
would.
:matches()
, on the other hand, is definitely a pseudo-class - it should only return a subset of the elements that the selector without :matches()
returns. Pseudo-elements thus don't really make sense in there.
Further, say you have something like .foo:matches(::before):hover
- what does that mean? Is it the same as .foo:hover::before
, or .foo::before:hover
(those are different and both valid!)? Normally we can arbitrarily re-order the non-pseudo-element simple selectors in a compound selectors, including pseudo-classes, but if ::before is allowed maybe we wouldn't be able to do that any more?
It seems to me that since pseudo-elements were allowed to be effectively the subject of pseudo-classing, we can't restrict the effect of pseudo-classing to "subsetting the _elements_ only" anymore. Yes, currently only pseudo-classes of user action are allowed for pseudo-elements, but I agree that @Loirooriol's example could be very useful in many cases.
Maybe it would be useful to introduce something like "any pseudo-element" notation that would be combinable with pseudo-classes like :mathches()
, :not()
and probably :is()
and would make the meaning of the selector unambiguous? E.g. something like .somewhere .something::*:matches(::before, ::after):hover
(meaning that :hover
belongs to the pseudo-element part, not the originating element)? And simply *:matches()
would always match only elements, so :matches(::before)
would match nothing, just like other valid-but-meaningless selectors like a:matches(b)
.
It seems to me that since pseudo-elements were allowed to be effectively the subject of pseudo-classing, we can't restrict the effect of pseudo-classing to "subsetting the elements only" anymore. Yes, currently only pseudo-classes of user action are allowed for pseudo-elements, but I agree that @Loirooriol's example could be very useful in many cases.
If I'm reading you correctly, I think you misunderstood what I meant by my previous comment. I'm not drawing any real distinction between elements and pseudo-elements; as far as Selectors is concerned, they're basically the same thing. (This is my entire point above; because a pseudo-element is effectively a new element, the pseudo-element selector is not equivalent in meaning to any other simple selector, and is instead basically a combinator into the "pseudo-children" of an element.)
Maybe it would be useful to introduce something like "any pseudo-element" notation that would be combinable with pseudo-classes
I wouldn't mind recasting things a bit to add a real combinator into the "pseudo-children", where presumably the pseudo-element names are instead matched as tagnames, like .foo :: before:hover
, or .foo :: *:matches(before, after)
, etc. I've looked into this before and there were some issues with parsing if we just reinterpreted ::
as a combinator; I'll have to dig up what they were, as I can't recall right now.
@tabatkins I don't say that elements and pseudo-element are _fundamentally_ different, and I like your interpretation of the ::
syntax as a "combinator", but in the same time I tend to see them as something conceptually close (though not the same, of course) to the elements from a different namespace. It's based on the fact that the usual universal selector (*
) doesn't select pseudo-elements by default, so we have to list them explicitly like *, *::before, *::after
(this is quite popular, for example, for changing the default box-sizing
to border-box
globally). And I assume that :matches()
is equivalent to *:matches()
, which leads me to conclusion that element:matches(::pseudo-element)
should match nothing because a particular element and its particular pseudo-element can't be the same object.
And I like the idea of treating ::
as a combinator! It seems to be consistent with the concept of Shadow DOM combinators. Perhaps only "legacy" pseudo-elements with single colon syntax are the parsing issue in this case?
Ah, I dug up the old thread (from 2012!).
The parsing problem with reinterpreting ::
as a combinator is indeed lethal. It's a one-two punch of backwards-compatibility.
First, selectors like .foo > ::before
exist today. If ::
is a combinator, then that's two combinators in a row. We'd have to additionally add a new ability to selectors - if you have multiple combinators in a row, it implies a *
selector between them. That way, the previous selector is identical to foo > * :: before
, which has the meaning we want.
However, this requires a new constraint - the descendant combinator (at least in its current whitespace form) can't play with this new chained-combinators feature, or else .foo > .bar
would suddenly start interpreting as three combinators in a row, meaning .foo * > * .bar
. That's not right! So we'd have to add in a new non-whitespace version of the descendant combinator to allow it to work with this feature.
But this causes another problem! Selectors like .foo ::before
exist today. Today, that's equivalent to .foo *::before
; with the new combinator-chaining feature and the descendant-combinator restriction, it changes meaning to become .foo :: before
(the before
pseudo-element of the .foo
element). This isn't acceptable!
So, we're screwed. Some existing selectors require a new feature to be parsed the same, which requires a funky restriction, which causes other existing selectors to not parse the same.
The only way around this is to add a totally new combinator for pseudo-children, not ::
, and that's much harder to sell as being worthwhile. :(
The CSS Working Group just discussed Why \"Pseudo-elements cannot be represented by the matches-any pseudo-class\"?
, and agreed to the following:
RESOLVED: Defer this issue to L5
The full IRC log of that discussion
<dael> Topic: Why \"Pseudo-elements cannot be represented by the matches-any pseudo-class\"?
<dael> github: https://github.com/w3c/csswg-drafts/issues/2284
<dael> fantasai: I wanted to ask if this is something we should try and define or close as no change
<dael> TabAtkins: Valid.
<dael> TabAtkins: I know that safari allows pseudo elements, but it doesn't make sense and I don't know why
<TabAtkins> .foo:matches(::before):hover
<dael> TabAtkins: This selector ^ bothers me.
<dael> TabAtkins: Unclear if it means before when foo is hovered or when foo.before is hovered.
<TabAtkins> .foo:matches(::before).bar is invalid, then
<dael> fantasai: If we're going to define we have to define if a selecotr ends in a pseudo element and has same meaning as if you placed element outside of matches.
<TabAtkins> .foo:matches(::before, .baz).bar is invalid, too
<dael> fantasai: That's invalid, right.
<dael> TabAtkins: Reasonable sort of thing to define on
<dael> TabAtkins: It's weird and I don't like it, but if it's what we want I'm okay
<dael> ericwilligers: What's the need for this?
<TabAtkins> .foo:matches(::before, ::after)
<dael> TabAtkins: Safari does it for selectors like ^
<dael> TabAtkins: You want to assign a style to multiple psuedo elements. Can't do that with matches right now. That's annoying, I agree
<dael> TabAtkins: Reasonable thing. Even bikeshed stylesheet would like to do a selector like this.
<fantasai> s/matches/is/ btw ;)
<TabAtkins> .foo:matches(::before, .bar)
<TabAtkins> ^ invalid
<dael> TabAtkins: Another option is we're more limited and you can put psuedo elements in if they're the only thing in the selector. All options have to be pseudo elements
<TabAtkins> .foo:matches(::before:hover) is valid
<dael> TabAtkins: That would be okay way to deal
<plinss> .foo:matches(::before):matches(:hover) ?
<TabAtkins> would be valid by my rule I think
<dael> fantasai: Thing that's tricky is different pseudo elements can be followed by different things. No need to make first example invalid b/c each branch is valid. If any branch is invalid whole is invalid.
<dbaron_> Pseudo elements seem like they might differ in what is allowed after them
<dael> fantasai: More restrictive I'm not sure that gains us a whole lot. You'll still need contextual checking.
<fantasai> s/checking/checking, even if they're all pseudo-elements/
<dael> TabAtkins: ericwilligers we don't support pseudo elements in matches right now, correct?
<dael> ericwilligers: Correct
<dbaron_> It seems like you might want to ensure that all branches of the :matches end in the same restriction state
<dael> TabAtkins: FF or Edge, how does this sound. Should I close no change and Safari violates spec? Or do we want?
<TabAtkins> dbaron_, I agree with that as a good restriction if we go this way
<dael> Rossen: Curious to hear from Safari folks.
<dael> Rossen: dbaron_ is on IRC [reads]
<dbaron_> (where we need to be conservative about what is the same, e.g. before and after OK but not selection)
<TabAtkins> :matches(::before, ::spelling-error) would be invalid
<dael> TabAtkins: before and after are fine, but before and spelling wouldn't work because allow different things
<dael> ericwilligers: Safari uses different selector name. So it doesn't matter too much.
<dael> TabAtkins: Not a backwards compat concern. It's what we want to do for the future
<dael> TabAtkins: I'm perfectly fine saying close no change
<dbaron_> Still have mixed feelings about whether to allow pseudos in v1
<dael> TabAtkins: dbaron_ is unsure. We can relax restriction later
<dbaron_> (typing on phone, BTW)
<dael> Rossen: smfr said Safari folks can't follow because they're in a meeting
<dael> Rossen: Objections to resolving no change?
<dael> fantasai: I think deferred
<dael> fantasai: Close no change means we don't want to accept. We might accept in future
<dael> fantasai: I'd prefer...if we're going to consider in the future we leave it open for selectors 5. If it's a baad idea we close no change.
<dael> Rossen: Don't disagree
<dael> Rossen: objections to defer to L5?
<dael> RESOLVED: Defer this issue to L5
(Copied from #4417)
In the past I've resisted things like .foo:is(.bar, ::before), because the ::before part is technically changing the subject of the compound selector, something no other selector can do.
But this just leaves a functionality gap, since pseudo-elements don't have a combinator dedicated to them to allow matching plainly. Selector syntax is awkwardly designed around pseudo-elements, but we're stuck with it, and should accept that pseudo-elements are part of the compound selector grammar and are allowed to change the subject in a real way.
Thus we should make sure that :not()/etc work properly with them too.
(There's no reason to use :not() with any of the current pseudos, but ::part(foo):not(::part(bar)) makes sense and is useful.)
I聽think ::*:is(before, after)
might聽be聽a聽solution to聽avoiding changing聽the聽subject of聽the聽compound聽selector, and聽in聽a聽way that聽would be聽compatible with聽::
聽being a聽combinator.
Rethinking the "nature" of pseudo-elements in terms of selector syntax (#5676), a new way to resolve the contradiction came to my mind...
the descendant combinator (at least in its current whitespace form) _can't_ play with this new chained-combinators feature, or else
.foo > .bar
would suddenly start interpreting as three combinators in a row
What if we declare the whitespaces around combinators other than ::
the optional part of these combinators themselves (i.e., the >
combinator would be defined as /\s*\>\s*/
in RegExp terms, and so on, with only ::
remaining simply /::/
for compat reasons)?
This change seems to make all the examples above unambiguous and retaining their current meaning: e.g. >
will always remain a single combinator regardless the number of whitespaces around it (unless these whitespaces are separated by an explicit *
), while > ::
will be parsed a sequence of two combinators, >
and ::
, leading to insertion of the implicit *
(by the new rule).
Most helpful comment
Ah, I dug up the old thread (from 2012!).
The parsing problem with reinterpreting
::
as a combinator is indeed lethal. It's a one-two punch of backwards-compatibility.First, selectors like
.foo > ::before
exist today. If::
is a combinator, then that's two combinators in a row. We'd have to additionally add a new ability to selectors - if you have multiple combinators in a row, it implies a*
selector between them. That way, the previous selector is identical tofoo > * :: before
, which has the meaning we want.However, this requires a new constraint - the descendant combinator (at least in its current whitespace form) can't play with this new chained-combinators feature, or else
.foo > .bar
would suddenly start interpreting as three combinators in a row, meaning.foo * > * .bar
. That's not right! So we'd have to add in a new non-whitespace version of the descendant combinator to allow it to work with this feature.But this causes another problem! Selectors like
.foo ::before
exist today. Today, that's equivalent to.foo *::before
; with the new combinator-chaining feature and the descendant-combinator restriction, it changes meaning to become.foo :: before
(thebefore
pseudo-element of the.foo
element). This isn't acceptable!So, we're screwed. Some existing selectors require a new feature to be parsed the same, which requires a funky restriction, which causes other existing selectors to not parse the same.
The only way around this is to add a totally new combinator for pseudo-children, not
::
, and that's much harder to sell as being worthwhile. :(