Why CSS :where() Specificity Is Always Zero
The CSS :where() pseudo-class is explicitly designed to
match elements while completely stripping away the specificity of any
selectors passed inside its argument list. Even when provided with
high-specificity selectors such as IDs or multiple classes—as seen in
:where(.a, #b)—the entire pseudo-class contributes a
specificity value of (0, 0, 0). This article explains the
technical mechanics behind this behavior according to the CSS
specifications and why it was intentionally designed this way.
The CSS Selectors Level 4 Specification
According to the W3C CSS Selectors Level 4 specification, functional pseudo-classes that take selector lists as arguments generally fall into two categories regarding specificity calculation:
- Dynamic specificity: Pseudo-classes like
:is(),:not(), and:has()take on the specificity of the most specific selector in their argument list. For example,:is(.a, #b)evaluates to the specificity of an ID selector:(1, 0, 0). - Zero specificity: The
:where()pseudo-class is defined to have a specificity of zero, regardless of the selectors placed inside it.
The specification explicitly overrides the normal cascading
calculation rules for :where(). When the browser's CSS
engine parses :where(.a, #b), it uses .a and
#b purely for element matching logic, while completely
nullifying their mathematical contribution to the cascade's weight
calculation.
The Purpose of Zero Specificity
Before :where() was introduced, managing specificity
wars in CSS was difficult, especially when building CSS resets, baseline
typography, or design systems.
Grouping selectors using traditional methods or the
:is() pseudo-class frequently created unintentionally high
specificity. If a developer wrote
:is(header, main, #featured) h2, every h2
matched would carry the weight of the #featured ID
selector, making it frustratingly difficult for downstream styles or
utility classes to override without resorting to !important
or chaining selectors.
The CSS Working Group introduced :where() specifically
to solve this problem. It acts as an escape hatch for specificity. By
forcing the specificity score to zero:
- Any baseline or layout styles applied via
:where()can be overridden by a standard single-class selector (.custom-heading). - It eliminates cascade conflicts in component libraries, allowing end-users to override vendor styles effortlessly.
- It enables complex selector groupings without artificially inflating the cascade weight of the target element.
How
:where(.a, #b) Behaves in Practice
When you write:
:where(.a, #b) {
color: red;
}The browser evaluates the rule as follows:
- Matching: The selector successfully matches any
element with the class
aor the IDb. - Specificity Calculation: The browser ignores the
class score (
0, 1, 0) and the ID score (1, 0, 0). The entire selector receives a score of(0, 0, 0).
Because the specificity is completely zero, a generic tag selector
defined elsewhere—such as p { color: blue; }—will take
precedence over :where(.a, #b) for any matching
<p id="b"> element, simply because a tag selector has
a specificity of (0, 0, 1), which is higher than zero.