
Simplifying Selector Specificity with CSS is and where
Anyone who has styled nested typography or interactive states in pure CSS knows the pain of combinator repetition. Selecting <h1>, <h2>, and <h3> headings inside <article>, <section>, or <aside> containers traditionally required writing out the full Cartesian product of nine comma-separated selectors—and worse, a single unsupported selector inside that comma-separated list would cause the browser to invalidate the entire rule block.
Defined in Selectors Level 4 and now landing across Safari, Firefox (Firefox 78+), and Chromium, the :is() and :where() functional pseudo-classes solve both problems:
- Both accept a forgiving selector list, ignoring any individual invalid selector inside the parentheses rather than dropping the entire rule.
:is()takes the specificity of its most specific argument, whereas:where()always has zero specificity (0, 0, 0), making it ideal for baseline design-system defaults that downstream utility or component classes can override cleanly.
Example
/* Traditional repetitive selector list (9 selectors) */
article h1, article h2, article h3,
section h1, section h2, section h3,
aside h1, aside h2, aside h3 {
line-height: 1.25;
}
/* Compact Selectors Level 4 equivalent using :is() */
:is(article, section, aside) :is(h1, h2, h3) {
line-height: 1.25;
}
Input : Repetitive compound CSS selectors and overly specific base reset rules
Output : Concise, forgiving selector groups with explicit control over cascade specificity.
Implementation
Understanding the specificity difference between :is() and :where() unlocks much cleaner architecture for CSS resets and reusable component libraries:
<article class="prose">
<h2 class="prose__heading prose__heading--hero">
Predictable Cascade Architecture
</h2>
<p>
Using <code class="inline-code">:where()</code> keeps baselineprose styles
at zero specificity so single-class modifiers always win.
</p>
<a href="#spec" class="prose__link">Read the W3C Selectors Level 4 Spec</a>
</article>
/* 1. Zero-specificity baseline styles using :where()
Specificity of `.prose :where(h1, h2, h3, h4)` is (0, 1, 0) — just `.prose`!
Without :where(), `.prose h2` would be (0, 1, 1) and `.prose #featured`
inside `:is(h2, #featured)` would jump to (1, 1, 0). */
.prose :where(h1, h2, h3, h4) {
margin-top: 1.75em;
margin-bottom: 0.5em;
color: #0f172a;
font-weight: 700;
}
/* Because the base rule above has (0, 1, 0) specificity, a single-class
modifier (0, 1, 0) declared later in the stylesheet overrides it cleanly
without needing `.prose .prose__heading--hero` or `!important`. */
.prose__heading--hero {
margin-top: 0;
color: #2563eb;
font-size: 2.25rem;
}
/* 2. Grouping interactive states cleanly with :is()
Specificity of `.prose__link:is(:hover, :focus)` is (0, 2, 0). */
.prose__link {
color: #0284c7;
text-decoration: underline;
text-underline-offset: 0.2em;
}
.prose__link:is(:hover, :focus) {
color: #0369a1;
background-color: #e0f2fe;
outline: 2px solid #0284c7;
outline-offset: 2px;
}
/* 3. Forgiving selector list behavior:
Even if an older browser does not recognize a newer pseudo-class inside
:is() or :where(), the remaining valid selectors in the list still match. */
:where(button, [role="button"], input:is([type="button"], [type="submit"])) {
cursor: pointer;
font: inherit;
}
Main Point: Use :is() to collapse repetitive compound selectors when normal specificity is desired, and reach for :where() in CSS resets and design-system base layers so default styles carry 0 specificity and never fight downstream class overrides.
Credits
Photo by Declan Sun