Here’s what I didn’t know about :where()

posted on

This is part 4 of my series Here’s what I didn’t know about… in which I try to learn new things about CSS. This time I'm trying to find out what I didn’t know about the :where() pseudo-class.


Okay, I’ll be honest. When I heard about the :where() pseudo-class for the first time, I wasn’t impressed because reading a rule like the following hurt my brain.

:where(header, main, footer) p:hover {
  color: red;
}

It feels like using a shorthand property instead of longhand properties; fewer lines of code, but harder to process. I didn’t get the point, but a few days ago it clicked.

Here’s what I didn’t know about :where():

You can use it to lower the specificity of a selector

I believe it was on Andys blog where I saw this smart rule:

ul[class] {
  margin: 0;
  padding: 0;
  list-style: none;
}

If a <ul> has no class, it’s probably a list in the true sense, so leave it untouched and display bullets. If the list has a class, it’s semantically still a list (in most browsers), but it’s likely that it doesn’t look like one, so reset the styling. Yeah, I know, dangerous assumptions, but it worked well for me in the past. The only issue I had with this solution is the high specificity caused by the combination of a tag and a class selector. If I wanted to change one of the default properties using just a class selector, it wouldn’t work.

<ul class="space">
  <li>HTML</li>
  <li>CSS</li>
  <li>JS</li>
</ul>
ul[class] {
  margin: 0;
  padding: 0;
  list-style: none;
}

.space {
  /* This doesn't apply because ul[class]
   has higher specificity than .space */
  margin: 2rem;
}

Open the first example on CodePen.

This is where :where() comes into play. No matter which selectors you pass to the pseudo-class, :where() will always have a specificity of 0.

ul:where([class]) {
  margin: 0;
  padding: 0;
  list-style: none;
}

.space {
  /* This applies because .space has higher 
  specificity than ul:where([class]) */
  margin: 2rem;
}

Even though the selector contains a tag, a pseudo-class and an attribute, only the tag selector adds to the specificity.

Open the second example on CodePen.

Another great example is input styling (thanks for the tip, Christopher).

input:where([type="text"], 
            [type="email"],
            [type="password"]) {
  border: 2px solid #000;
}

[aria-invalid] {
  border-color: red;
}

or if you're a fan of the :not() selector:

input:where(
  :not(
    [type="button"], 
    [type="reset"],
    [type="image"]
  )
) {
  border: 2px solid #000;
}

Open the third example on CodePen.

:where() is a forgiving selector

If a list of selectors contains an invalid selector, none of the selectors will match.

<button>
  Send
</button>
/* The background color won't change on :hover or :focus
 because the invalid :touch pseudo-class makes the whole
list invalid  */
button:focus,
button:hover,
button:touch {
  background: red;
}

Open example 4 on CodePen.

If you use :where() instead, :focus and :hover will still match.

button:where(:focus,
             :hover,
             :touch) {
  background: red;
}

Open example 4 on CodePen.

Conclusion

I haven’t used :where() in production yet, but considering that browsers support is pretty great, I’m going to try it out soon. The only concern I still have is the cognitive overload selectors like :where() or :not() create.