Tip of the week #015: The :where selector
CSS has gotten a lot of new selectors in the last couple of years. Here's how the :where selector works, and why you should use it.
The :where
selector looks like this:
:where(h1,h2,h3) {
font-weight: 700;
}
As you can see, you can group several selectors within the :where
selector, instead of comma separating them this way:
h1, h2, h3 {
font-weight: 700;
}
At first glance this looks like an overcomplication, but they're actually not the same. They have different specificity.
Let's look at specificity
For those unfamiliar with specificity this page goes into more details, but in simple terms it's all about which selector trumps other selectors.
<h1 class="heading" id="heading">Hello World</h1>
<style>
h1 {
color: green; /* Fourth place */
}
#heading {
color: red; /* Wins */
}
h1.heading {
color: yellow; /* Second place */
}
.heading {
color: blue; /* Third place */
}
</style>
In this example, the ID selector (#heading
) wins because it's more specific than the element-with-class selector (h1.heading
), the simple class selector (.heading
) and the tag selector (h1
).
Specificity is a gift and a curse. It's one of the reasons why CSS sometimes feel like this:
So where does :where come into play?
:where
is useful if you want to create overridable defaults or resets. The :where
selector let's you group other selectors and give them the same styling, whilst removing all of their specificity.
Let's say you have this scenario:
p,
h1,
h2,
h3 {
margin-block: 0.5em;
}
.rich-text > p,
.rich-text > h1,
.rich-text > h2,
.rich-text > h3 {
margin-block: 0;
}
.rich-text > * + * {
margin-block-start: 1em;
}
- The tags
p
,h1
,h2
andh3
have margins added to its block direction - You have a class called
.rich-text
where you reset the margin of the same tags, if they are direct descendants - You add a lobotomized owl selector to the
.rich-text
class, so that every adjacent descendant gets a margin, making the text flow neatly
This will result in the following (I've added colors so that you more easily can spot the overrides):
As we can see, the selector .rich-text > p
trumps the selector .rich-text > * + *
because > p
has a higher specificity than > * + *
.
This is not what we want. Therefore, we can use :where
to create a lower specificity for our margin reset, and allow the lobotomized owl selector to trump this:
p,
h1,
h2,
h3 {
margin-block: 0.5em;
}
.rich-text > :where(p, h1, h2, h3) {
margin-block: 0;
}
.rich-text > * + * {
margin-block-start: 1em;
}
I like to look at the :where
selector as a "mild" setting, which you allow to be overwritten by any other specificity when needed. It's therefore very useful as a reset or default.
In this case the .rich-text > :where()
selector will trump the simple p, h1, h2, h3
selector because it's still looking for direct descendants of .rich-text
, but it will not be as intrusive as .rich-text > * + *
— which is what we want.
Here's the final result:
In conclusion
Apart from being a different way to combine several selectors, it's useful for when you want to write a simple reset or default.
I want to point out that :where
is very similar to the :is selector, but they are very different. I have to write another post about the :is
selector, as it's too much to go into any detail here.