Using new pseudo-class selectors in 2023

CSS has introduced some new pseudo-class selectors: :is(), :has(), :where(). Before we get to read more about these cool new pseudo-class selectors bear in mind that they are new and not all browsers might support them yet.

What is a pseudo-class selector?

Let’s start by explaining what a pseudo-class selector is. A CSS pseudo-class selector is used to add styles to a selector but only when a certain condition is met. A pseudo-class selector is always added by adding a colon after the selector followed by a pseudo-class. Some examples of commonly used pseudo-class selectors are :hover and :focus:

h1:hover {
    text-decoration: underline;
}

:is()

The :is()(formerly :matches(), formerly :any()) selector is really cool and lets you group multiple selectors and apply the same styles to each of them. It allows you to avoid manually writing out all combinations as seperate selectors and results in a similar effect to nesting in Sass and most other CSS preprocessors:

:is(ul, ol) li {
    list-style-type: none;
    font-size: 20px;
}

:is(article) h1,
h2,
h3,
h4 {
    font-size: 30px;
}

Here we select all list items that are children of ul or ol elements and all h1, h2, h3, and h4 elements that are children of the article element. Generally without :is() we’d have to do the following to achieve the same result:

ul li,
ol li {
    list-style-type: none;
    font-size: 20px;
}

article h1,
article h2,
article h3,
article h4 {
    font-size: 30px;
}

I know this doesn’t seem too bad right now but imagine having a huge codebase where you’d have to style a lot of elements with general selectors. This could be a really nice and reliable way to solve an issue like that.

:has()

The :has() pseudo-class selector is a selector that gets added to the parent. However, it actually styles elements based on which children they have. How cool is that? This selector makes it possible to only apply styles to the selected element when it has a certain child. This makes it a pretty powerful pseudo-class selector. Let’s say you’d only want to style article elements which include an image:

article:has(img) {
    margin-bottom: 16px;
}

This code only adds a margin-bottom to article elements that contain an img element.

Selecting previous siblings

It was impossible for the longest time to select previous siblings in CSS. The :has() pseudo-class selector actually makes this possible. Let’s say we have have the following markup:

<p class="text">...</p>
<p class="text"></p>
<img class="image" src="..." alt="..." />
<p class="text"></p>
<p class="text"></p>

And we only want to style the .text element that comes before the .image element. In order to style this we can use the adjacent sibling selector(+). This selector is able to select an element that immediately follows another item. Combing this with :has() makes it possible to only select the .text that’s followed by a .image which is the previous sibling of .image:

.text:has(+ .image) {
    margin-bottom: 16px;
}

The above CSS will only style the .text right before the .image.

There are way more possibilities for previous sibling selector elements than just this one, if you’d like to read more on this I highly recommend you to check out this article by Tobias Ahlin Bjerrome.

:where()

The :where() pseudo-class selector looks and feels a lot like the :is() selector. The difference between the two is that :is() is usually really specific, while :where() is much more general. The specificity of this selector will always be zero:

article:where(h1, h2, h3, h4) {
    font-size: 30px;
}

instead of:

article h1,
article h2,
article h3,
article h4 {
    font-size: 30px;
}

Where would a selector like this come in handy you ask? Well since the specificity of this selector is always zero, it means that this selector is pretty forgiving. MDN explains this as follows:

“The difference between :where() and :is() is that :where() always has 0 specificity, whereas :is() takes on the specificity of the most specific selector in its arguments.”

In the following example we have an article containing a heading and two sections. Each section contains a list with list items. The list-items all contain a span element with some content. Let’s use :is() and :where() to select the spans:

:is(section.is-styling) span {
    color: red;
}

:where(section.where-styling) span {
    color: green;
}

But what if we want to override this color later on in our code:

section span {
    color: purple;
}

The red spans will stay the same color because the selectors inside :is() will count to the specificity of the overall selector. But since the selectors inside :where() have zero specificity, the span inside :where() will turn purple.

Forgiving selector list

MDN also explains that both :where() and :is() accept something called a forgiving list selector. Usually when using a CSS selector list, the whole list is invalid when one of the selectors is invalid. The beauty of using :is() or :where() is that if one or more selectors are invalid, the invalid selectors will be ignored and the others will still be used. MDN uses the following example to showcase this:

:where(:valid, :unsupported) {
    /* … */
}

“Will still parse correctly and match :valid even in browsers which don’t support :unsupported, whereas:”

:valid,
:unsupported {
    /* … */
}

“Will be ignored in browsers which don’t support :unsupported even if they support :valid.”

Specificity madness

So when would you use what you ask? It can be quite hard for developers to understand how specificity is determined when using :is(), :has() and :where() since they all calculate specificity differently . This tool can help to gain some insight regarding specificity.

Browser support

As stated at the start of this article not all browsers are supporting these features yet. Here I’ve listed the browser support found as listed on caniuse.com

:is()

:is() is pretty widely supported on all newer browsers and has a global usage rate of 95%:

:is browser support

check out the current browser support on canisue

:where()

:where() is also pretty widely supported on all newer browsers and has a global usage rate of 90%:

:where browser support

check out the current browser support on canisue

:has()

:has() seems to be the lesser supported of the three and has a global usage rate of 86%:

:has browser support

check out the current browser support on canisue

Sources