skip to content

Why would the author of a CSS reset or component library wrap its base selectors in :where(), and what does that choice cost?

level: seniorimportance: should knowfreq 34%

answer

  1. base styles are supposed to lose
  2. weightless by definition, matching unchanged
  3. a plain type selector can beat it
  4. no !important needed downstream
  5. losing every fight cuts both ways

basics

~20 s

Wrapping base selectors in :where() drops them to zero specificity, so any consumer declaration — even a single type selector — overrides them without escalation or !important. The cost is that the library can never win a conflict, including against a consumer's accident.

solid answer

~40 s

A reset or base layer has to lose every fight, and specificity is what usually prevents that. A base rule written as `.prose ul li { margin: 0 }` scores `(0,2,1)` and outweighs a consumer's plain `.menu-item`, pushing them into longer selectors or `!important`. Wrapping it — `:where(.prose ul li) { margin: 0 }` — makes the whole selector score `(0,0,0)`, so literally any authored declaration beats it and the only remaining tiebreak among other zero-weight rules is source order. The cost is symmetric: the library gives up the ability to win anything. A stray consumer rule silently overrides base styles with no signal, and internal ordering inside the library itself becomes the only lever left, so a broad `:where()` rule later in the file can quietly beat a narrower one earlier.

code

css · 7 lines
css
:where(.prose ul li) {
  margin-block: 0;
}

.menu-item {
  margin-block: 0.5rem;
}

go deeper

for a junior

Know that :where() contributes no specificity, so a rule written inside it is easy for later CSS to override. Recognising the pattern in a reset stylesheet is enough at this level.

for a middle

Explain the arithmetic: .prose ul li scores (0,2,1) while :where(.prose ul li) scores (0,0,0), and only the arguments inside the function are zeroed, so partial wrapping still carries weight.

for a senior

Argue both sides in production terms — consumers override with a plain class and never reach for !important, but the library can no longer defend any default and internal source order becomes the only remaining tiebreak.

for a principal

Own the convention itself: whether shared CSS ships defaults at zero specificity, how that interacts with the ordering guarantees the codebase relies on, and how you keep the choice legible to reviewers who see two nearly identical selectors in a diff.

## The problem base styles have A reset, a normalizing layer, or a component library's default styling exists to be replaced. The author does not know what the consumer will write, only that whatever it is should win. Specificity works against that goal, because the selectors that make base styles *correct* are often the ones that make them *heavy*. Consider a typography base that has to scope itself so it does not leak: ```css .prose ul li { margin-block: 0; } /* (0,2,1) */ ``` A consumer writing `.menu-item { margin-block: 0.5rem }` scores `(0,1,0)` and loses. Their options are all bad: lengthen their selector to outweigh the library, add `!important`, or reach for an ID. Each of those raises the floor for the *next* override, and a codebase where every override escalates ends up with selectors nobody dares to touch. ## What :where() changes `:where()` contributes zero specificity regardless of its contents. Wrap the entire selector and the rule drops to the floor: ```css :where(.prose ul li) { margin-block: 0; } /* (0,0,0) */ ``` The matching behaviour is unchanged — the same list items are targeted — but any authored declaration now beats it. `li { margin-block: 0.5rem }` at `(0,0,1)` wins. A bare type selector defeating a three-part scoped selector is exactly the inversion the library author wants. Note the wrapping must cover everything you want zeroed. `:where(.prose) ul li` still scores `(0,0,2)` because the two type selectors sit outside the function; `:where()` zeroes only its own arguments. ## The costs, honestly stated **You give up winning entirely.** There is no such thing as a base rule that loses to intentional overrides but survives accidents. A consumer's unrelated `ul { padding: 2rem }` will clobber a carefully considered default with no warning and no diagnostic — the styles simply do not appear, and the debugging session that follows starts from "why is this rule greyed out in devtools". **Ordering becomes the only lever inside the library.** Once every internal rule scores `(0,0,0)`, source order is the sole tiebreak among them. A general rule placed after a specific one silently wins, which is the opposite of the intuition most authors carry from ordinary stylesheets. Libraries that zero everything must be disciplined about file order in a way that ordinary stylesheets are not. **It hides intent from readers.** `:where(.prose ul li)` and `.prose ul li` look almost identical in a diff, and a reviewer who does not know the convention will not register that one of them cannot win. Applying it selectively — some rules wrapped, others not — produces a stylesheet whose cascade behaviour cannot be inferred from reading it. **It does not solve override order across sources.** `:where()` only removes specificity from the comparison. It says nothing about which stylesheet loads first, and it does not protect the library from being loaded *after* the consumer's CSS. ## Where it fits, and where something else does The sweet spot is genuinely bottom-of-the-stack defaults: element normalization, typographic rhythm, and component base styles that exist purely so an unstyled page looks sane. It is also useful for adding *context* for free — `:where(.theme-dark) .button` scores the same `(0,1,0)` as `.button` alone, so a theme qualifier does not start an escalation war. What `:where()` is not is a general answer to override ordering. It gives a rule zero weight; it does not give you a way to say "this whole group of styles should lose to that whole group". Cascade layers exist for that grouping problem, and the two mechanisms are complementary rather than substitutes. ## What a strong answer sounds like A candidate who has actually shipped this says three things: the mechanism (zero specificity by definition, matching unchanged), the payoff (consumers override with a plain class and never need `!important`), and the honest cost (the library can no longer defend any default, and internal source order becomes load-bearing). Saying only the first two is the tell that the technique was read about rather than lived with.

  • Does wrapping in :where() change which elements the rule matches?
    No. `:where()` and `:is()` have identical matching behaviour; only the specificity contribution differs. `:where(.prose ul li)` targets exactly the same list items as `.prose ul li` — it just scores `(0,0,0)` instead of `(0,2,1)`, so it loses every conflict rather than winning most of them.
  • If every rule in a stylesheet scores zero, what decides conflicts between them?
    Source order, and nothing else — the last matching declaration wins. That inverts the usual intuition, because a broad rule placed later silently beats a narrower one written earlier. A library that zeroes everything therefore has to treat file and import order as load-bearing, not incidental.
  • How would you debug a base style that mysteriously stops applying?
    Inspect the element and look for the overridden declaration struck through in the rules panel, then read the winning selector's weight. With zero-specificity base rules the winner is often something trivially light — a bare type selector — which is the expected outcome of the design, not a bug, so the fix belongs in the consumer's CSS rather than in the base layer.

saying these in an interview costs you the question

  • Claiming :where() changes which elements a rule matches
  • Saying :where() prevents overrides rather than enabling them
  • Thinking :where() zeroes the whole selector when only part is wrapped
  • Assuming zero specificity also removes source-order effects
  • Treating :where() as a replacement for !important discipline everywhere

context