The Specificity Escape Hatch Nobody Asked For

All the attention on :is() lately is well deserved. It finally landed in Safari 14, which means it's usable everywhere, and it solves a real pain point: writing verbose, repetitive selector chains. But the interesting part isn't just the convenience—it's what the selector does to specificity.

Here's the quick recap of why :is() matters:

  • Its selector list is forgiving—if one selector in the list fails, the others still apply.
  • Its specificity is equal to the most specific argument inside it.
  • It doesn't work with pseudo-element selectors, at least for now.

That specificity rule creates some unusual opportunities. You can, for example, use :is() to inflate the specificity of a rule without actually targeting anything extra:

:is(.button, #increase#specificity) {
  /* specificity is now (0, 1, 0, 0) instead of (0, 0, 1, 0)
}

That's a trick I've resorted to in the past with other selectors:

.button.button.button {
  /* forcing the selector to be (0, 0, 3, 0) instead of (0, 0, 1, 0) */
  /* doesn't actually require element to have three button classes lol */
}

The :is() version is arguably more readable. But what if you need to go in the opposite direction and drop specificity altogether? That's where its sibling :where() comes in. Functionally, it behaves identically: same comma-separated list, same forgiving nature, same matching logic. The only difference is that the entire :where() portion contributes zero to the specificity calculation.

That distinction becomes crucial when you have competing rules. Consider this situation that Kevin Powell highlighted:

.card :is(.title, p) {
  color: red;
}

.card p {
  color: yellow;
}

You'd expect yellow to win, but it doesn't. The top selector has a .title class inside its :is(), which pushes its specificity to (0, 0, 2, 0)—beating the bottom rule's (0, 0, 1, 1).

Swap in :where() and the math changes:

.card :where(.title, p) {
  color: red;
}

.card p {
  color: yellow;
}

Now yellow wins, because the top rule's specificity drops to (0, 0, 1, 0), losing to the bottom's (0, 0, 1, 1).

Which One Do You Reach For?

There's no time-tested playbook yet. The good news is that both tools exist, so you can pick based on the situation. Generally, low specificity is the healthier default—it leaves room to override later without fighting yourself. But zero is an extreme low, and that can cause its own confusion when you're trying to figure out why a rule isn't applying. A reasonable starting point is :is(); if you find yourself mixing in a higher-specificity selector and it causes problems, back off to :where().