Description
The original version of :is()
, :-webkit-matches()
, allowed pseudo-elements in its argument list. So you could write .foo:-webkit-matches(*, ::before, ::after)
, and it would be equivalent to .foo, .foo::before, .foo::after
.
We ended up removing this, because it conflicts with the Selectors data model - simple selectors, like pseudo-classes, only ever filter the current set of matched elements; only combinators can change the set to new elements (and pseudo-elements). The pseudo-element "selector" is a legacy syntax mistake from the early days of CSS, and today would have been instead done as a type of combinator (as discussed in #7346, which still might happen).
But this means that you can't easily write useful selectors like the above. You can write them with Nesting, but you can't then nest further: .foo { *, &::before, &::after {...}}
is fine, but .foo, .bar::before { &:hover {...}}
won't match any hovered ::before pseudos, despite .bar::before:hover
being a valid and meaningful selectors, because it's treated identically to :is(.foo, .bar::before):hover
, which drops the .bar::before
arg from the :is()
.
This isn't great! And it makes Nesting less useful for future cases of nested pseudo-elements, in addition to anything built on Nesting concepts, like Mixins probably will be.
I don't have an answer ready for this, I just needed to raise it for broader discussion and thought.
Some loosely-ordered thoughts:
- The three syntactic elements of Selectors are simple selectors (filter the matched elements), combinators (change the matched elements), and lists (union the matched elements).
- Possibly we recognize a fourth - the "complex selector unit" which filters and might change the subject to a pseudo-element.
:is()
is trying to replicate the functionality of lists, but pulling it into the syntactic space of simple selectors, thus the conflict. You can combine:is()
with other simple selectors, so the final subject of each selector inside of:is()
needs to be the same element.- If we did allow pseudo-elements in there and kept the data model correct, it would be unintuitive; we'd effectively have some selector reordering. That is,
:hover:is(*, ::before)
would have to be equivalent to:hover, ::before:hover
, not:hover, :hover::before
. - But that sort of re-ordering is in conflict with things like
.foo:is(::before, ::after)
, wanting to avoid repeating the preceding selector. And simple selector order can't matter;.foo:is()
and:is().foo
have to be identical. So that also prevents reasonable stuff like.foo:is(::before, ::after):hover
from working. - I feel like we need to somehow invent a way to wedge "lists" into the syntax of a single selector, rather than being restricted to the top level of a selector as it is now. The problem isn't the branching that :is() allows, it's the fact that :is() is a simple selector, so we can't care about order/etc.
- Spitballing: naked parens indicate a list. Lists can change the subject, and stuff can't get reordered across them. Like
.foo(*, ::before, ::after):hover
. - Nope,
foo(
is a function token. I need the parens to be separate from other bits of the compound selector, but I also need to still be able to glom a compound selector before or after it. - Second try:
.foo = (*, ::before, ::after) = :hover
.=
is a new combinator that doesn't change the set of matched elements at all; it's a no-op.(...)
is a new term in the<complex-selector-unit>
grammar, that takes a selector list and matches any of them. Since it's part of a complex selector and guaranteed to be surrounded by combinators, we avoid the issues from earlier.- So in this example, we (1) select
.foo
elements, (2) without changing the set of matched elements, select*, ::before, ::after
elements (aka the element, plus their before/after pseudos), (3) without changing the set of matched elements, select:hover
elements. - This works with other combinators, too:
.foo > (*, ::before)
selects all children of a.foo
element and their before pseudos. - The selectors inside the
()
can be full complex selectors, just like in:is()
, so.foo = (.bar *, #baz > *)
selects.foo
elements that have a.bar
ancestor or a#baz
parent. - This is still slightly weaker than Nesting itself; you can't do the equivalent of
.foo { &, & > .bar {...}}
because the current match set (whatever the preceding combinator yielded) is always the subject of the selectors in the paren list. That's probably okay. We'd have to invent a new way to refer to the elements being matched if we wanted to expand that (not:scope
or&
, but a secret third way). I dunno, maybe we do need it. - Specificity would have to be the same as
:is()
, to avoid the same sort of "which way did it match" exponential explosion. - Nesting can then define itself on top of this -
X { Y {...}}
desugars to(X) = (Y) {...}
, with the magic ability to use&
to explicitly refer to the current match elements. If I'm thinking about this correctly, this is a no-op change except that it allows::before, ::after { &:hover {...}}
to work, where today it doesn't.- Ah, no, the fact that you'd take the highest specificity from the list means that
(X) = (Y)
is different - currently we do care about which way you matched the last bit. We'd probably just make that magic, then - I can't see a reasonable way to define()
behavior to get that back in general. Nesting would just have the special behavior that it does care about the branch you actually matched in the final bit. - Also, we'd have to define that the relative ordering of
&
still doesn't matter in a compound selector, and it still "represents the elements matched by the parent selector", so::before, ::after { &:hover {...}}
and::before, ::after { :hover& {...}}
mean the exact same thing. (Both equivalent to::before:hover, ::after:hover {...}
.)
- Ah, no, the fact that you'd take the highest specificity from the list means that