Clean Markup

Simplifying Selector Specificity with CSS is and where

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:

  1. Both accept a forgiving selector list, ignoring any individual invalid selector inside the parentheses rather than dropping the entire rule.
  2. :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

← Home