skip to content

In CSS, how does the specificity of :is() differ from :where(), and how is each one computed?

level: middleimportance: must knowfreq 72%

answer

  1. same matching, different weight
  2. one of them always scores zero
  3. most specific argument sets the weight
  4. weight does not depend on which argument matched
  5. 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 s

They 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
css
:is(#main, .sidebar) p { color: red; }    /* (1,0,1) */
:where(#main, .sidebar) p { color: blue; } /* (0,0,1) */

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context