In CSS, how does the specificity of :is() differ from :where(), and how is each one computed?
answer
- same matching, different weight
- one of them always scores zero
- most specific argument sets the weight
- weight does not depend on which argument matched
- outside the function still counts
basics
~10 s:is() takes the specificity of its most specific argument, applied to every match. :where() always contributes zero specificity no matter what it contains. Both match identically; only their weight in the cascade differs.
solid answer
~40 sThey match exactly the same elements — the only difference is weight. `:is()` contributes the specificity of its **most specific argument**, and it does so uniformly: `:is(#main, .sidebar) p` scores `(1,0,1)` even when the element actually matched via `.sidebar`, because the ID inside the list sets the weight. `:where()` is the zero-specificity twin: whatever you put inside it contributes nothing, so `:where(#main, .sidebar) p` scores `(0,0,1)` — just the `p`. Note the selector *outside* the function still counts normally in both cases. That makes `:where()` the tool for base, reset, and library styles you want authors to override with a single plain class, and `:is()` the tool for compact grouping where you accept the heaviest argument's weight for the whole group.
code
css · 2 lines:is(#main, .sidebar) p { color: red; } /* (1,0,1) */
:where(#main, .sidebar) p { color: blue; } /* (0,0,1) */go deeper
Memorise the one-line difference: :is() carries the weight of its heaviest argument, :where() carries none. Being able to state that cleanly is what a screening question is checking.
Be ready to compute triples out loud for selectors like :is(#main, .sidebar) p and :where(.a) .b, and to explain that :is() resolves weight statically from the list rather than from the argument that matched.
Show when you would reach for each in production: :where() for resets, base layers and free scoping qualifiers; :is() for compact same-weight groups. Explain how :where() removes the pressure to escalate to !important.
Own the policy angle — whether shared or design-system CSS ships its defaults at zero specificity so product teams never have to fight it, and how that choice interacts with the ordering rules the codebase already relies on.
## The specificity triple, briefly A selector's specificity is a three-part value usually written `(a, b, c)`: `a` counts ID selectors, `b` counts classes, attribute selectors and pseudo-classes, and `c` counts type selectors and pseudo-elements. The parts are compared left to right, so any ID beats any number of classes. Both functions below are scored against that same triple; they change what the *function* contributes, not how the comparison works. ## :is() — the weight of the heaviest argument `:is()` matches an element that matches any selector in its list, and it contributes the specificity of its **most specific argument**. Crucially, that weight is fixed for the whole selector — it does not depend on which argument actually matched at run time: ```css :is(#main, .sidebar) p { color: red; } /* (1,0,1) — always */ ``` A `p` inside `.sidebar` is styled by a selector scoring `(1,0,1)`, exactly as if it had matched through `#main`. That is the single most surprising thing about `:is()` and the reason mixing an ID with classes inside one `:is()` is usually a mistake: everything in the group inherits the ID's weight, and overriding it later needs an ID-strength selector. This is also where `:is()` diverges from the longhand selector list it appears to abbreviate. A comma list is evaluated per selector: `#main p` scores `(1,0,1)` and `.sidebar p` scores `(0,1,1)`, independently. The `:is()` form flattens both to the higher value. ## :where() — always zero `:where()` is identical in matching behaviour and takes a selector list the same way, but its specificity is **always zero**, regardless of its contents: ```css :where(#main, .sidebar) p { color: red; } /* (0,0,1) — only the p counts */ ``` Nothing inside `:where()` is ever counted. That includes IDs, classes, attribute selectors, and further nested functions. What is *outside* the function still counts normally, which is the detail people most often get wrong: `:where()` zeroes its arguments, not the whole selector. ## Worked comparisons ```css a:is(.btn, .link) { } /* (0,1,1) — a plus one class */ a:where(.btn, .link) { } /* (0,0,1) — just a */ :is(.a, .b, .c) .item { } /* (0,2,0) — one class from :is(), one from .item */ :where(.a) :is(#x) .y { } /* (1,1,0) — :where() zero, :is() the ID, plus .y */ ``` ## Why :where() exists Zero-weight selectors solve a real problem: a base stylesheet that must lose. If a reset declares `ul { margin: 0 }` at `(0,0,1)`, a page author's `.menu { margin: 1rem }` at `(0,1,0)` already wins — but a reset that needed a more elaborate selector, such as `.prose ul li`, would outweigh a plain consumer class and force escalation or `!important`. Wrapping the qualifying parts in `:where()` drops the reset to the floor: ```css :where(.prose ul li) { margin-block: 0; } /* (0,0,0) */ ``` Now literally any authored declaration beats it, including a single type selector, and the only remaining tiebreak among equally weighted rules is source order. ## Practical selection rule Use `:is()` when you want compact grouping and you are comfortable with the heaviest argument's weight applying to all of them — typically groups of same-kind selectors like `:is(h1, h2, h3)` or `:is(.card, .panel)`. Use `:where()` when the rule is a default that downstream code must be able to override effortlessly, or when you want to add qualifying context to a selector without paying for it in specificity. A related trick: because `:where()` is free, you can add scoping context without escalating weight. `:where(.theme-dark) .button` still scores `(0,1,0)`, the same as `.button` alone, so a theme qualifier does not start a specificity war. ## Support Both pseudo-classes shipped in all major browsers by 2021 and need no fallback in new work. They are supported wherever `:has()` is, and considerably longer.
- If an element matches `:is(#main, .sidebar) p` through the class, does it still get ID-level specificity?Yes. `:is()` resolves its weight statically from the most specific argument in the list, not from whichever argument matched at run time. A `p` inside `.sidebar` is styled at `(1,0,1)`, so overriding it later needs an ID-strength selector — which is why mixing an ID with classes inside one `:is()` is usually a mistake.
- Does :where() zero out the whole selector it appears in?No — only its own arguments. `:where(.a) .b` still scores `(0,1,0)` because `.b` sits outside the function. To make an entire selector weightless, every part of it has to be inside `:where()`, as in `:where(.a .b)`, which scores `(0,0,0)`.
- What is the specificity of a :where() containing an !important declaration's selector?Still zero — `!important` is not part of specificity at all. It moves the declaration into a different, higher-priority origin bucket, and only if two important declarations from the same origin collide does specificity break the tie. A zero-specificity selector with `!important` still beats an ordinary declaration.
saying these in an interview costs you the question
- Claiming :is() always has zero specificity
- Saying the weight depends on which argument matched
- Thinking :where() zeroes the entire selector, not just its arguments
- Assuming :is() and :where() match different elements
- Treating :where() as a way to disable inheritance or the cascade