CSS Functional Pseudo-Classes: is, where, has, not… what?

Alex Salmon
AppLearn Engineering
5 min readNov 23, 2022
Cascading Street Stairs? Photo by Karim MANJRA on Unsplash

The new CSS functional pseudo-class syntax aims to deliver greater legibility and shorter, more powerful selectors. In this post, we’ll review these four new CSS selectors with practical examples.

is, where, has, not… what?

Function Syntax

Firstly, we’ll look at what’s shared between them.

Each of these functional pseudo-class selectors start with : and end with () , which act as function calls to match elements from a selector list:

parent-selector:is(selector-list) { ... }
parent-selector:where(selector-list) { ... }
parent-selector:has(selector-list) { ... }
parent-selector:not(selector-list) { ... }

These selectors allow for targeting groups of elements together from the start, middle or end of a selector. They also can scale the selector specificity, easily allowing an increase or squashing of specificity.

Selector List

A Selector List is a comma-separated list of selectors. These lists are usually used together as a group to share the same declarations:

p { color: red }
span { color: red }
/* Equivalent Selector List */
p, span { color: red }

Selector Lists are used as the arguments of our functional pseudo-class selectors.

Now, let’s dive in to what each of these functional pseudo-classes is and does.

1. :is()

:is() is the simplest selector to demonstrate the benefits of, and aims to reduce the repetition of nested CSS selectors.

By passing a selector list, :is() can select a large range of elements from compact syntax.

Here’s an example: selecting headings which may need to be styled specifically when nested inside semantic elements:

/* Before :is() */
article h1,
article h2,
article h3 {
color: #000;
}
/* After :is() */
article :is(h1, h2, h3) {
color: #000;
}
------/* Before :is() */
article h1,
section h1,
nav h1 {
font-size: 3rem;
}
/* After :is() */
:is(article, section, nav) h1 {
font-size: 3rem;
}

We can see that the repetition of selectors has been reduced, resulting in simpler and more legible code.

Regarding specificity, the :is() pseudo-class matches the specificity of its most specific argument. Therefore, a selector containing :is() may not have equivalent specificity to the equivalent selector written without :is().

You can learn more about :is() on the Mozilla dev blog here.

:is() Browser Support

2. :where()

:is() and :where() are syntactically interchangeable.

/* Before :is */
article h1,
section h1,
nav h1 {
font-size: 3rem;
}
/* After :is() */
:where(article, section, nav) h1 {
font-size: 3rem;
}

However, the key difference is in their specificity::where() has zero specificity.

A specificity of zero will never make a style more specific, which can be useful in maintaining a lower general specificity in our styling.

A practical use for this zero specificity is when providing styles from a third-party library. The library may want to provide some default styling, without forcing the consumer to have to heavily specify to overwrite this default styling.

We can see that the default heading colors for the library-component are blue. However, as the library-component selectors are listed within the :where() functions, their specificities are considered to be zero; this allows almost any other selectors to apply, regardless of the cascade (i.e. the order in which they were defined).

Note that “zero specificity” is still enough specificity to override the browser agent’s own default styles. This makes :where() a great option to softly use in CSS Resets:

*:where(:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)) {
all: unset;
display: revert;
}
:where([hidden]) {
display: none;
}

Again, you can learn more about :where() here.

:where() Browser Support

3. :has()

Sometimes referred to as the “Parent Selector”, :has() can be used to apply styles to an element depending on whether relative conditions are met or not. These conditions can include checking the elements own nested child elements:

parent:has(child) { color: red; }

In this example, the style color: red will only be applied to the parent, if the child is nested within.

This enables the parent to apply styles based on what is inside of it, allowing styling to be less repetitive when needing to handle various states of content.

The following example applies a border to the figure, only if there is a figcaption nested inside.

Like :is(), the specificity of :has() is its most specific argument.

Unfortunately, :has() is the least supported functional pseudo-class, but should be worth the wait!

More about :has() over here.

:has() Browser Support

4. :not()

Finally, :not() allows us to not match a list of selectors:

parent:not(.active, [disabled])

By combining :not() with other selectors, it’s possible to create complex UI styling without the use of JavaScript:

In this example, hovering over a menu link will reduce the opacity of the other menu links, for an incredibly exciting UI microinteraction 🤩.

This is completed by combining :has() and :not() to apply styling specifically to the nav a child elements which are not being hovered:

nav:has(a:hover) > a:not(:hover) { ... }/*
1. For `nav` element,
2. match when an `a` element is hovered,
3. then apply styles to `a` elements which are not hovered.
*/

This may seem complex at first, but allows the menu to be styled without any JavaScript, effortlessly allowing matching styling for keyboard users:

nav:has(a:hover) > a:not(:hover),
nav:has(a:focus) > a:not(:focus) { ... }

It’s advisable to keep the usage of :not() relevant to simple cases, as it represents a “negation pseudo-class” and can introduce a complex paradigm into calculating styling a specificity.

And if you want to know more about :not() you know where to go at this point (here).

:not() Browser Support

Conclusion

Although the potential for complex selectors is slowly increasing, these selectors are going to simplify a lot of styles which currently require repetition or additional JavaScript.

With a considered approach, once awkward styling challenges can be achieved with minimal implementation.

One step closer to cleaner CSS code.

:is(the-end) :has(learnt-some-cool-stuff) :not(forget-to-clap-clap-clap)

--

--