Home / Articles / Modern CSS /

new-feature

:is() and :where() selectors

Two pseudo-classes that reduce selector repetition. They look identical but have a critical difference — specificity.

Reducing selector repetition

Before :is(), matching the same rule for multiple parents meant writing every combination by hand.

/* old way — verbose */
article h2, section h2, aside h2 {
  color: oklch(35% 0.12 260);
}

/* with :is() — compact */
:is(article, section, aside) h2 {
  color: oklch(35% 0.12 260);
}

Specificity: the key difference

:is() takes the specificity of its most specific argument. :where() always contributes zero specificity.

/* :is() specificity = (1, 0, 1) because #main is an ID */
:is(#main, footer) p { color: oklch(30% 0 0); }

/* :where() specificity = (0, 0, 1) — only the trailing p counts */
:where(#main, footer) p { color: oklch(50% 0 0); }
Rule of thumb: use :where() for defaults you expect consumers to override, and :is() when you want normal specificity behavior.

Forgiving selector lists

Both :is() and :where() use forgiving parsing. If one selector in the list is invalid, the rest still work. A regular comma-separated selector list would discard the entire rule.

/* :unsupported-pseudo is invalid, but the rule still applies to .card */
:is(.card, :unsupported-pseudo) {
  border: 1px solid oklch(80% 0 0);
}

/* without :is(), the entire rule is dropped */
.card, :unsupported-pseudo {
  border: 1px solid oklch(80% 0 0); /* ignored! */
}

Practical patterns

These patterns show up constantly in production codebases.

/* style all headings inside prose */
.prose :is(h1, h2, h3, h4) {
  font-family: var(--font-display);
  letter-spacing: -0.02em;
  color: oklch(25% 0.05 260);
}

/* reset-layer defaults with :where() */
@layer reset {
  :where(ul, ol) { list-style: none; padding: 0; }
  :where(a) { color: inherit; text-decoration: none; }
}

/* interactive states */
.btn:is(:hover, :focus-visible) {
  background: oklch(50% 0.25 270);
}

Using with CSS nesting

Native CSS nesting implicitly wraps parent selectors in :is(). Understanding this prevents specificity surprises in nested rules.

.card {
  padding: 1.5rem;

  /* compiles to :is(.card) h2 — specificity (0, 1, 1) */
  h2 { font-size: 1.25rem; }

  /* compiles to :is(.card):hover — specificity (0, 2, 0) */
  &:hover {
    box-shadow: 0 4px 12px oklch(0% 0 0 / 0.1);
  }
}
Because nesting uses :is() under the hood, a nested .card h2 may have different specificity than the same selector written flat. Keep this in mind when debugging.

Browser support and summary

Both :is() and :where() are supported in all modern browsers since 2021. There is no reason to avoid them in new projects.

  • Use :is() for general selector grouping.
  • Use :where() for low-specificity defaults and resets.
  • Leverage forgiving parsing when mixing stable and experimental selectors.
  • Be aware of specificity escalation when :is() contains IDs.