Pseudo-elements are not valid inside :is(), :where(), and &

A pseudo-element is not a valid selector inside :is() or :where(). As per the Selectors Level 4 spec:

Pseudo-elements cannot be represented by the matches-any pseudo-class; they are not valid within :is().

The nesting selector & behaves the same way. It desugars to the parent rule’s selector list wrapped in :is().

However, neither of them fails loudly. :is() and :where() take a forgiving selector list, so an invalid argument is dropped and the rest of the rule carries on. Similarly, nesting inside a pseudo-element rule parses fine. The nested style rule just never matches anything, unless the parent list also contains non-pseudo-element selectors or there are bare declarations nested inside an at-rule.

/* exmaple 1 */
.card::before {
	content: '';

	.dark & {
		color: red; /* `&` can't represent `.card::before`, so this matches nothing */
	}
}

Either hoist the element out to the top level.

/* exmaple 2 */
.card::before {
	content: '';
}

.dark .card::before {
	color: red;
}

Or restructure so & refers to the element and the pseudo-element is nested in it.

/* exmaple 3 */
.card {
	&::before {
		content: '';
	}

	.dark &::before {
		color: red;
	}
}

A pseudo-element in the parent selector list doesn’t poison the whole list. & drops just that entry and still represents the rest.

/* exmaple 4 */
.card,
.card::before {
	color: black;

	.dark & {
		color: red; /* matches `.card`, but not `.card::before` */
	}
}

The nested rule desugars to .dark :is(.card, .card::before), and the forgiving list throws out the pseudo-element, leaving .dark :is(.card). So the element turns red while its ::before stays black.

From Chrome 130 / Safari 18.2 / Firefox 132 and onwards, bare declarations nested inside an at-rule are the exception. They do apply to the pseudo-element.

/* exmaple 5 */
.card::before {
	content: '';
	color: black;

	@media (prefers-color-scheme: dark) {
		color: white; /* applies to `.card::before` */
	}
}

Wrap those same declarations in & { ... } and they stop applying.

/* exmaple 6 */
.card::before {
	content: '';
	color: black;

	@media (prefers-color-scheme: dark) {
		& {
			color: white; /* matches nothing */
		}
	}
}

So why does example 6 not work?

The reason is that declarations can’t sit loose inside a nested at-rule as far as the CSSOM is concerned. A CSSMediaRule exposes rules, and declarations only live on a style rule, so the parser has to wrap them in something. Examples 5 and 6 differ in what that something is.

Example 5 wraps them in a nested declarations rule:

It matches the exact same elements and pseudo-elements as its parent style rule, with the same specificity behavior.

Whereas example 6 wraps them in & { ... } and & obeys selector rules: it desugars to :is(.card::before), which can’t represent the pseudo-element, so nothing matches.

Further reading:

Posted on

← Backto the previous page