Summary

  • A February 2018 W3C draft described an unnamed zero-specificity cousin of :matches(); the November draft named it :where() and illustrated why a precise link filter should sometimes carry no extra weight.
  • :where() matches like :is() but contributes zero specificity for itself and its arguments. Selectors written outside its parentheses still count; the author can choose how strongly a reusable rule should compete with local CSS.

The hyperlink is minding its own business until the stylesheet meeting begins. A general rule, a:not(:hover), removes underlines from links that are not being hovered. A navigation author later writes nav a to underline links in a menu. Both selectors have a type selector. The first also carries a pseudo-class, so its weight beats the navigation rule’s two type selectors. The menu author moves the second rule lower in the file. The underline remains missing. At this point many a CSS file has acquired a more aggressive selector, an !important, and the faint smell of a workplace grievance.

This is the example in the W3C’s 21 November 2018 Selectors Level 4 draft. Its repair is small enough to fit on a sticky note: write a:where(:not(:hover)). The condition still selects links that are not hovered. Its specificity contribution disappears inside :where(), leaving the outer a with type-selector weight. Now nav a can win for navigation links. The author of the general rule has kept the behavior and surrendered an accidental veto. A useful default has learned how to be a guest.

The idea arrived before its name. The 2 February 2018 draft recorded a zero-specificity :matches()-type pseudo-class and marked its name as undecided. By November, the draft had the :where() name and a distinct neighbor called :is(); the CSS Working Group’s publication note announced both changes. That sequence is revealing. The problem was not finding more elements. CSS already knew how to match. The problem was allowing a reusable match to express its own restraint in the cascade.

Selectors do two jobs that are easy to fuse in one’s head. First they decide which elements a rule applies to. Then their specificity helps decide which competing declaration wins. :where() splits the two jobs. Put a filter inside it and the filter can be as descriptive as the markup requires without charging specificity for that description. The 7 May 2022 draft makes the arithmetic blunt: .qux:where(em, #foo#bar#baz) has specificity (0,1,0). The intimidating IDs inside the parentheses count for matching; the .qux outside still counts for weight. Zero is a deliberate setting for the function, not a magic eraser for the whole selector.

That distinction changes how a design system can speak. A baseline stylesheet may want to style a family of controls, or a reading surface’s links, without forcing every consuming page to produce a selector with longer arms. A precise default can say “apply here” while also saying “a component author nearby may revise this.” The browser supplies the shared arithmetic. The people shipping each page decide which rules should enter with their elbows tucked in. This is a modest form of local control, one declaration at a time.

The November 2018 text even gives the feature a name suited to that social arrangement: specificity adjustment. It does not require the navigation author to know every detail of the base rule, nor the base-rule author to foresee every navigation bar. They can meet through the cascade. If the general rule had stayed bossy, each new component would need to win an escalation contest. A selector arms race is a poor substitute for an interface between teams.

The 11 November 2022 draft kept the zero-weight rule and set out a related parsing detail: a forgiving selector list can discard an invalid item while retaining valid ones. That helps explain why the functional-selector family became a useful place for evolving CSS syntax. The practical promise remains bounded by the specification’s actual parsing rules; a malformed stylesheet does not receive diplomatic immunity merely by walking through :where().

The W3C publication history dates the drafts, while the drafts show the design move. February 2018 had the unnamed seat at the table. November gave it a name and a link whose underline became the test case. The 2022 text kept the zero and made the calculation explicit. CSS got a selector that can identify an element without demanding seniority over everyone who styles it later. In a language famous for arguments about who wins, this one knows when to stop talking.