Este contenido solo está disponible en Inglés.

También disponible en Español.

Ver traducción
Frontend

CSS :where() and :is(): Specificity, Resets, and Maintainability

Many developers overlook the CSS pseudo-classes :where() and :is(), seeing them as "syntax sugar". This article reveals how crucial they are for solving specificity chaos, enabling you to create scalable and maintainable CSS. Understand the difference and apply them in resets, components, and design systems.

Equipe Blueprintblog7 min
CSS :where() and :is(): Specificity, Resets, and Maintainability

Have you ever written the same CSS block three times, only changing the selector? That's exactly the pain that :is() and :where() solve — and the difference between the two is more important than it seems.

Does this CSS look familiar to you:

text
header a,
footer a,
nav a {
  color: blue;
  text-decoration: none;
}

Repeating the same rule for three different contexts. Now imagine you have ten contexts. And each one has three state variations — :hover, :focus, :visited. The CSS file turns into a phone book.

Modern CSS has an answer for this. Two, actually — and they look the same but behave differently in a detail that will save you from subtle bugs.

:is() — group selectors without repetition

The :is() accepts a list of selectors and applies the style to any one that matches. The block above becomes:

text
:is(header, footer, nav) a {
  color: blue;
  text-decoration: none;
}

One line, same result. The browser interprets this exactly like the repeated version — semantically they are equivalent.

It gets more interesting when you combine it with pseudo-classes:

text
/* Before — repeating for each state */
button:hover,
button:focus,
button:active {
  background: darkblue;
}

/* After — clean */
button:is(:hover, :focus, :active) {
  background: darkblue;
}

Or to style headings all at once:

text
/* All headings within article */
article :is(h1, h2, h3, h4) {
  font-family: Georgia, serif;
  line-height: 1.3;
}

:where() — identical, but without specificity weight

Here lies the difference that matters.

Minimalist flat design, technical diagram, clean vector illustration, dark theme, professional tech blog style. A comparison showing CSS specificity. Left side: A block representing ':is()' with a large 'S' (Specificity) icon and an arrow pointing from a sub-element (e.g., '#id') to the main block, indicating 'Inherits highest specificity'. Right side: A block representing ':where()' with a '0' icon, indicating 'Zero Specificity'. Use different colored shapes for specificity levels (e.g., red for high, green for zero). NOT include text, explain concepts visually.
Minimalist flat design, technical diagram, clean vector illustration, dark theme, professional tech blog style. A comparison showing CSS specificity. Left side: A block representing ':is()' with a large 'S' (Specificity) icon and an arrow pointing from a sub-element (e.g., '#id') to the main block, indicating 'Inherits highest specificity'. Right side: A block representing ':where()' with a '0' icon, indicating 'Zero Specificity'. Use different colored shapes for specificity levels (e.g., red for high, green for zero). NOT include text, explain concepts visually.

Specificity in CSS is the scoring system that decides which rule wins when two contradict each other. An ID selector is worth more than a class, which is worth more than a tag. When you use :is(), the specificity of the strongest selector inside the parentheses propagates to the entire rule.

text
/* :is() inherits the specificity of #header — high */
:is(#header, .nav, footer) a {
  color: blue;
}
/* ↑ this selector has ID specificity
   because of #header, even if the element
   is .nav or footer */

The :where() works the same in behavior — but it has zero specificity. Always. No matter what you put inside.

text
/* :where() — zero specificity */
:where(#header, .nav, footer) a {
  color: blue;
}
/* ↑ easy to override — any class
   or tag selector already wins */

In practice, this means that :where() is ideal for base styles and resets — you define them without locking in the developer who will customize them later.

Minimalist flat design, technical diagram, clean vector illustration, dark theme, professional tech blog style. A side-by-side comparison illustrating CSS rule overriding. Left side: 'Problem with :is()'. Show a base rule (e.g., a gray rectangle for 'color: gray') defined using ':is()' with a large 'S' (specificity) icon, blocking a subsequent, simpler rule (e.g., a black rectangle for 'color: black') represented by a smaller 'S' (specificity) icon, with a crossed-out arrow indicating failure to override. Right side: 'Solution with :where()'. Show a base rule (gray rectangle) defined using ':where()' with a '0' (zero specificity) icon, and a subsequent, simpler rule (black rectangle) represented by a small 'S' (specificity) icon successfully overriding it, with a checkmark. NOT include text, explain concepts visually.
Minimalist flat design, technical diagram, clean vector illustration, dark theme, professional tech blog style. A side-by-side comparison illustrating CSS rule overriding. Left side: 'Problem with :is()'. Show a base rule (e.g., a gray rectangle for 'color: gray') defined using ':is()' with a large 'S' (specificity) icon, blocking a subsequent, simpler rule (e.g., a black rectangle for 'color: black') represented by a smaller 'S' (specificity) icon, with a crossed-out arrow indicating failure to override. Right side: 'Solution with :where()'. Show a base rule (gray rectangle) defined using ':where()' with a '0' (zero specificity) icon, and a subsequent, simpler rule (black rectangle) represented by a small 'S' (specificity) icon successfully overriding it, with a checkmark. NOT include text, explain concepts visually.

The real-world difference

Problem with :is()
/* base.css */
:is(article, section) p {
  color: gray;
}

/* theme.css — will not work */
p {
  color: black;
}
/* :is(article, section) p has
   higher specificity than p */
Solution with :where()
/* base.css */
:where(article, section) p {
  color: gray;
}

/* theme.css — works */
p {
  color: black;
}
/* :where() has zero specificity
   any p wins in an override */

Modern CSS resets use :where() for this reason. Tailwind, Open Props, and several popular resets use :where() to define base styles that never lock in the developer. The intention is: "this is the default, but anything you write easily overrides it."

An example that combines both

In a design system, you would use both in different layers:

text
/* Base layer — :where() to avoid locking anyone in */
:where(h1, h2, h3, h4, h5, h6) {
  margin: 0 0 0.5em;
  line-height: 1.2;
}

/* Component layer — :is() for specific contexts */
.card :is(h2, h3) {
  font-size: 1.25rem;
  color: var(--color-heading);
}

/* Interactive states — :is() because weight matters */
.btn:is(:hover, :focus-visible) {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

Be careful with invalid selectors inside :is(). If you place an invalid selector in a comma-separated list in traditional CSS, the entire rule is ignored. With :is() and :where(), invalid selectors are silently discarded — the others continue to work. This is an advantage, but it can hide typos.

What you should take away from this

  • :is() groups selectors and inherits the specificity of the strongest one in the list.
  • :where() does the same but with zero specificity — always overridable.
  • Use :where() in resets and base styles that should not lock in customization.
  • Use :is() in components and interactive states where specificity matters.
  • Both eliminate selector repetition — your CSS becomes smaller and more readable.
  • Browser support: all modern browsers. No compatibility concerns.

Etiquetas del articulo

Articulos relacionados

Recibe los ultimos articulos en tu correo.

Follow Us: