is() and where() for Deliberate Specificity Control
AI generated
{ }
@
CSS · Selectors · Architecture
is() and where() for Deliberate Specificity Control
Zero specificity, migration patterns and the end of !important

is and where are usually introduced as a plain shortcut for long selector lists, yet their biggest value lies in specificity control. where() creates zero specificity for resets and design systems, is() inherits the highest specificity of its argument, without developers having to spell out every combination manually.

18 min read is selector · where selector · specificity All modern browsers

1. Why is and where are specificity tools, not shortcuts

Most introductions present is and where as a convenient shortcut: :is(h1, h2, h3) replaces three separate rules with a single one. That is correct, but it hides the actual strategic value of the two pseudoclasses. The decisive difference between them lies not in syntax but in specificity: :is() inherits the specificity of the most specific argument, while :where() always has a specificity of exactly zero, regardless of how complex the argument is.

This property makes is and where deliberate tools for specificity architecture, not just a way to avoid repetition. Anyone who understands that :where() actively resets specificity to zero can write base styles that can be overridden by practically any other rule in the project, entirely without !important. The following sections show how this deliberate specificity control is applied in practice, from resets through design systems to migrations out of BEM heavy legacy CSS.

2. The specificity model refreshed briefly

CSS specificity is usually represented as a triple of three numbers: ID selectors count in the first category, classes, attribute selectors and pseudoclasses in the second, element and pseudo element selectors in the third. A selector such as #header .nav a:hover would therefore have a specificity of one, two, one. In a conflict between two rules, the one with the higher category wins, regardless of order in the stylesheet, and only on complete equality does source order decide.

is and where intervene directly in this calculation, but in opposite ways. :is(.card, #special) would inherit the specificity of #special, because that is the higher of the two alternatives, completely regardless of which of the two parts actually matches. :where(.card, #special), on the other hand, has a specificity of zero in every case, even with an ID present in the argument. This understanding is the foundation for every deliberate application of the two pseudoclasses.


/* :is() inherits the highest specificity among its arguments */
:is(.card, #special) {
  /* Specificity here equals #special: (1,0,0) */
  padding: 1rem;
}

/* :where() always has zero specificity, regardless of the argument */
:where(.card, #special) {
  /* Specificity here is (0,0,0), no matter what */
  padding: 1rem;
}

/* Practical consequence: :where() is trivially overridable */
.card { padding: 2rem; } /* wins over :where(.card, #special) */

3. where(): zero specificity for resets and base styles

The most practical use case for :where() is writing base styles that should deliberately stay easily overridable. A classic CSS reset, for example, sets margin: 0 on headings, paragraphs and lists. Done through normal selectors like h1, h2, h3 { margin: 0; }, this specificity adds up to everything that later needs to be overridden in the project. With :where(h1, h2, h3) { margin: 0; }, specificity stays exactly zero, and every single class rule elsewhere in the project can override the reset without effort.

This pattern is especially valuable for design system libraries or base stylesheets that are reused by other teams or in other projects. A library that writes its base styles with :where() guarantees that consumers of the library never have to fight unexpectedly high specificity. Without :where(), library authors would either have to use very low, often too generic selectors, or force their users to work with even higher specificity or !important to override base styles.


/* Reset base styles with zero specificity, trivially overridable */
:where(h1, h2, h3, h4, h5, h6) {
  margin: 0;
  font-weight: inherit;
}

:where(ul, ol) {
  list-style: none;
  padding-inline-start: 0;
}

/* Any single class anywhere in the project wins automatically */
.article-title {
  margin-block: 1rem; /* overrides the :where() reset without effort */
}

4. is(): inherited specificity without repetition

While :where() actively neutralizes specificity, :is() behaves differently: it saves writing effort without changing the specificity calculation. :is(.sidebar, .footer) a:hover has exactly the same specificity as if .sidebar a:hover, .footer a:hover had been written out separately, because :is() inherits the specificity of whichever argument is highest, and in this case both arguments share the same category.

The advantage of :is() shows especially in deeply nested selector combinations, where writing out every combination by hand quickly becomes unwieldy. :is(.card, .panel, .widget) :is(h2, h3) { color: var(--heading-color); } covers six combinations with a single, readable rule. It is important that :is() does not change a project's specificity strategy, it merely makes writing existing specificity more compact and maintainable.

5. Recipe: design system layers without specificity conflicts

A growing design system typically has several layers: base tokens, component base styles, and project specific adjustments. Without deliberate specificity control, these layers quickly compete with each other, especially when component libraries were accidentally written with high specificity. With :where(), the component base layer can be deliberately kept at zero specificity: :where(.ds-button) { padding: 0.5rem 1rem; border-radius: 0.375rem; } ensures that any project specific adjustment, no matter how simply written, wins automatically.

At the same time, :is() can be used to compactly define variants within the same design system layer without losing the zero specificity guarantee: :where(.ds-button):is(.ds-button--primary, .ds-button--secondary) { font-weight: 600; } keeps specificity at zero, because the outer :where() shell dominates overall specificity. This combination allows complex, well structured design system selectors that still never compete with project specific CSS.


/* Design system base layer: zero specificity, always overridable */
:where(.ds-button) {
  padding: 0.5rem 1rem;
  border-radius: 0.375rem;
  border: 1px solid transparent;
}

/* Variants nested inside :where() keep the zero-specificity guarantee */
:where(.ds-button):is(.ds-button--primary, .ds-button--secondary) {
  font-weight: 600;
}

/* Any project-level class overrides the design system without !important */
.checkout-button {
  padding: 0.75rem 1.5rem; /* wins automatically */
}

6. Recipe: migrating BEM overrides without !important

Many grown projects struggle with BEM selectors that have grown over years into chains like .block__element--modifier.is-active .child. Every new requirement led to even more specific selectors or straight to !important, because the existing specificity was otherwise impossible to overcome. A migration toward :where() based base styles allows breaking this spiral step by step, without rewriting the entire stylesheet at once.

The practical path: write new components from the start with :where() for base styles, while existing BEM chains stay untouched. Since :where() rules are practically overridden by any class rule, no new specificity conflicts arise with the legacy code, even when both systems coexist in the same project. This coexistence makes :where() a low risk entry point for a long term specificity cleanup, without forcing a complete rewrite.

7. Combining with cascade layers

Cascade layers via @layer and is and where solve related but different problems: layers control which rule group wins in a conflict, regardless of specificity within the group, while :where() controls specificity within a single rule. Combined, a two stage control emerges: @layer reset { :where(h1, h2, h3) { margin: 0; } } places the reset in the lowest layer priority while also keeping internal specificity at zero.

This combination is especially robust because it uses two independent mechanisms instead of relying on a single one. Even if a developer accidentally writes an ID heavy rule into the reset layer, layer ordering ensures that rule still loses against all later layers. :where() and cascade layers therefore complement each other rather than duplicating, forming a considerably more robust system than specificity alone.

8. Pitfalls: where where() does not actually lower specificity

A common misunderstanding is that :where() affects the specificity of elements outside its own argument. :where(.card) .title has the specificity of .title alone, a single class, not zero, because only the part inside the parentheses is set to zero, the rest of the selector counts normally. Anyone writing :where() only around a partial expression, assuming this lowers overall specificity, will be surprised by this behavior.

A second pitfall concerns nested combination with :is(): :where(.card):is(.featured, #special) keeps the outer zero specificity of the :where() shell, but only because :where() actually sits outside. Swap the order to :is(.card):where(.featured, #special) and specificity is instead determined by :is(.card), not zero. The position of the pseudoclasses in the selector directly decides which part is actually neutralized.

9. is() and where() compared directly

The following table summarizes when each of the two pseudoclasses is the right choice for deliberate specificity control.

Goal :is() :where() Recommendation
Avoiding repetition Suitable Also possible, changes specificity is() when specificity should stay unchanged
Always overridable base styles Inherits existing specificity Suitable where() for zero specificity
Design system library Risky with high argument specificity Suitable where() guarantees overridability
CSS reset Raises specificity like a normal rule Suitable where() is the standard choice for resets
Compact combinatorics without specificity change Suitable Unintentionally sets specificity to zero is() preserves existing structure

The rule of thumb: use :where() whenever a rule should deliberately stay easily overridable, use :is() whenever only writing effort should be saved without changing the existing specificity logic. Together they form a precise tool pair for deliberate specificity architecture, instead of retroactive repair with !important.

Mironsoft

CSS architecture, design systems and legacy migration

Stylesheets without !important cascades and specificity fights?

We analyze grown specificity problems in existing projects and migrate base styles deliberately toward a where() based architecture, for maintainable, conflict free stylesheets.

Specificity Audit

Identifying !important clusters and specificity conflicts in the codebase

Design System Setup

where() based base layer for new or growing component libraries

Cascade Layer Strategy

Combining @layer and where() for robust, two stage cascade control

10. Summary

is and where solve a shared selector repetition problem, but with a decisive difference in their effect on specificity. :is() inherits the highest specificity of its argument and saves writing effort without changing existing specificity logic. :where() actively resets specificity to zero, regardless of the argument, making it the deliberate choice for resets, design system base layers, and any rule that should deliberately stay easily overridable.

Combined with cascade layers, a two stage control over the cascade emerges: layer order for rule groups, :where() for the internal specificity of individual rules. Anyone using these tools deliberately can write base styles that never fight project specific CSS, and migrate existing BEM heavy projects step by step without falling back on !important.

is() and where() Specificity Control: The Key Points at a Glance

is()

Inherits the highest specificity of its argument, saves writing effort without changing specificity.

where()

Always has zero specificity, ideal for resets, design systems and easily overridable base styles.

Position in the Selector

Only the part inside where() is set to zero, the rest of the selector counts normally.

Cascade Layers

@layer plus where() gives two stage control, more robust than specificity alone.

11. FAQ: is() and where() Specificity Control

1Main difference regarding specificity?
is() inherits the highest argument specificity, where() always has zero specificity.
2When where() instead of is()?
For resets, design system base layers, or libraries that should stay easily overridable.
3Does where() affect the rest outside parentheses?
No, only the part inside the parentheses is set to zero, the rest counts normally.
4Can both be combined?
Yes, but order decides which pseudoclass dominates overall specificity.
5Does where() replace !important?
In many cases yes, since zero specificity is automatically overridden by any class rule.
6Combination with cascade layers?
@layer for rule group priority, where() for internal specificity, together two stage control.
7Good for BEM migration?
Yes, write new components with where(), existing BEM chains stay untouched.
8Does is() increase specificity?
No, it inherits exactly what separate selectors would have anyway.
9Typical mistake with where()?
Assuming it lowers the whole selector's specificity instead of just its own parenthesized part.
10Browser support?
Both are supported by all current evergreen browsers and considered production safe.