Lesson 4 of 30
Two selectors that look interchangeable and mostly are, except for one specificity difference that decides whether your reset is easy to override or a fight waiting to happen.
10 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
`:where(#hero) p { color: gray; }` is written. What specificity does this rule actually have?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Both :is() and :where() let you group multiple selectors into a single compact rule instead of repeating a shared descendant selector for each one:
/* repetitive */
.card h2, .panel h2, .modal h2 { margin-top: 0; }
/* grouped */
:is(.card, .panel, .modal) h2 { margin-top: 0; }
Both produce the same match — any h2 inside a .card, .panel, or .modal. The difference between them is entirely about specificity, and it's the one thing worth actually remembering:
:is() takes on the specificity of its most specific argument. :is(.card, #hero) behaves, specificity-wise, as if you'd written the whole rule with #hero's specificity (an ID) — even for the elements matched through .card.:where() always contributes zero specificity, regardless of what's inside it. :where(.card, #hero) h2 has the exact same specificity as plain h2 — the grouped selectors decide what matches, but contribute nothing to how hard it is to override./* a reset: consumers should be able to override this with a single class,
so it should contribute zero specificity */
:where(ul, ol) {
list-style: none;
margin: 0;
padding: 0;
}
/* a component's grouped interactive states: specificity here is meant to behave
like each selector was written out, so :is() is the right call */
:is(.btn:hover, .btn:focus-visible) {
outline: 2px solid var(--color-accent);
}
The reset uses :where() on purpose: a consumer writing a plain .my-list { list-style: disc; } can override it with ordinary specificity, because the reset contributes none. If that reset had used :is() instead, ul's specificity (a type selector) is low either way here, but the general lesson holds — the moment a reset groups anything with higher specificity inside :is(), overriding it gets harder than it should be. The component's hover/focus rule uses :is() because there's no reason to want it easy to accidentally override — its specificity should behave normally.
:where() always contributes zero specificity, even with an ID inside it. :where(#hero) p has the specificity of a plain type selector (p), not an ID selector — this surprises people the first time, since #hero looks like it should matter.:is() takes the specificity of its highest-specificity argument, not an average or the first one listed. :is(.a, .b, #c) behaves like an ID selector for every branch, even the ones matched through .a or .b.:is(.card > h2, .panel > h2) — they're not limited to simple class lists.:is(.card, .panel) h2 and :where(.card, .panel) h2 match exactly the same elements; only which one wins a later conflict differs."When would you use :where() instead of :is()?" is a real, common question, and the answer that shows real understanding is the specificity-zeroing behavior — recognizing that :where() exists specifically so reset and base styles stay easy to override, which is a deliberate design choice, not an oversight or a lesser version of :is().