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.
Table of Contents
- 1. Why is and where are specificity tools, not shortcuts
- 2. The specificity model refreshed briefly
- 3. where(): zero specificity for resets and base styles
- 4. is(): inherited specificity without repetition
- 5. Recipe: design system layers without specificity conflicts
- 6. Recipe: migrating BEM overrides without !important
- 7. Combining with cascade layers
- 8. Pitfalls: where where() does not actually lower specificity
- 9. is() and where() compared directly
- 10. Summary
- 11. FAQ
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.