Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

:is() and :where(): Selector Lists Without the Copy-Paste

C
Carlo Fontanos
· 2 min read

You've written this stylesheet stutter:

article h1, article h2, article h3,
section h1, section h2, section h3 {
    scroll-margin-top: 80px;
}

Six selectors for one idea. With :is(), the idea gets written once:

:is(article, section) :is(h1, h2, h3) {
    scroll-margin-top: 80px;
}

:is() matches if any selector in its list matches - it distributes like multiplication over addition. Two short lists replace the six-way cross product, and adding h4 later is a one-word edit instead of two.

:where() - the same thing with a superpower disguised as a weakness

:is() takes the specificity of its most specific argument. :where() always contributes zero. That sounds like trivia until you write base styles other people (including future you) need to override:

/* Base link styling with ZERO specificity weight from the :where part */
:where(.prose, .comment, .card-body) a {
    color: #4f46e5;
    text-underline-offset: 2px;
}

/* Later, anywhere: a plain class wins - no !important, no specificity war */
.danger-link { color: crimson; }

Had that base rule used :is(.prose, .comment, .card-body), its specificity would include a class, and .danger-link would lose. This is why every modern CSS reset is wrapped in :where() - defaults that yield gracefully. If you ship any kind of base layer or theme, :where() is your politeness tool: full styling power, no specificity footprint blocking downstream overrides.

The forgiving list: one bad selector no longer kills the group

Classic CSS drops an entire rule if any selector in the comma list fails to parse - the silent killer when mixing vendor pseudo-elements:

::-webkit-slider-thumb, ::-moz-range-thumb { ... }   /* BOTH die in every browser */

:is(::-webkit-slider-thumb)... /* (pseudo-elements aren't valid in :is, bad example there) */

Inside :is()/:where(), unknown or unsupported selectors are simply ignored while the rest match on - "forgiving" parsing. (Pseudo-elements specifically stay outside; the forgiveness applies to element/class/pseudo-class selectors, including experimental ones you're feature-testing.) Writing :where(:has(img), .has-img) style fallback pairs is safe in a way plain comma lists never were.

Everyday combos

.menu > :is(a, button)              { padding: 8px 14px; }        /* mixed children, one rule */
:is(h2, h3, h4):hover .anchor-link  { opacity: 1; }               /* shared hover behavior */
input:is(:focus-visible, .invalid)  { border-color: currentColor; } /* state OR class */

Support: everywhere since 2021. These two read like syntax sugar, but :where()'s zero-specificity behavior is genuinely strategic - it's the difference between a design system people extend and one they fight.

C
Written by Carlo Fontanos

Full-stack web developer sharing practical tutorials and building tools that ship.

Got something on your mind?

My inbox is open - no forms disappearing into the void here.

  • Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
  • Hire me for a project Have something custom in mind? Let's talk scope and timelines.
  • Product support Bought something here? I'll help you get it running.

I usually reply within 1-2 business days.

Message sent!

Your details are only used to reply to you.

Keep reading