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:

  1. 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).
  2. 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:

  1. Matching: The selector successfully matches any element with the class a or the ID b.
  2. 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.