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%:
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%:
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%:
check out the current browser support on canisue
Sources
- :is
- :has
- :where
- forgiving-selector-list
- :is browser support
- :where browser support
- :has browser support
- Selecting previous siblings with CSS :has()
- CSS specificity calculator/