:is() and :where() both let you collapse a repetitive group of selectors into one, so you write the shared part once instead of pasting it in front of every sibling. They look interchangeable, and for matching they are. The one thing that actually decides which to reach for is specificity: :is() takes on the specificity of its most specific argument, while :where() always counts as zero. That single difference is the whole reason both exist.
Here is the repetition they kill. This is the kind of selector block every stylesheet grows:
header nav a:hover,
header nav a:focus,
header nav a:active {
color: var(--accent);
text-decoration: underline;
}The header nav a prefix is repeated three times. With :is() you write it once and group the states inside:
header nav a:is(:hover, :focus, :active) {
color: var(--accent);
text-decoration: underline;
}That is the headline feature: factor the common ancestor out, list the varying parts inside the parentheses. It reads better, and adding a fourth state is a one-word edit instead of another full selector line.
The specificity difference is the whole point
Matching-wise, header nav a:is(:hover, :focus) and header nav a:where(:hover, :focus) select exactly the same elements. What differs is how hard the resulting rule is to override.
:is()adopts the specificity of its most specific argument.:is(#sidebar, .panel, nav)weighs as much as#sidebar, an ID, because that is the heaviest thing in the list. This can surprise you: stuffing one ID into an:is()list quietly raises the specificity of the entire rule.:where()always has zero specificity. It contributes nothing. The rule's weight is whatever sits outside the:where().:where(#sidebar, .panel, nav)adds nothing to the score no matter what you put inside it.
Worked example. Suppose I ship a design-system default and a consumer wants to override it with a plain class:
/* design-system default */
:where(article) h2 {
color: slategray;
}
/* a consumer's override, written later */
.featured h2 {
color: crimson;
}Because :where(article) contributes zero, the default rule has the specificity of a single element selector (h2), and the consumer's .featured h2 (a class plus an element) wins cleanly. No !important, no specificity arms race. That is the killer use for :where(): resettable defaults. You set sensible baselines that any author-level rule beats without thinking about it.
Use :is() instead when you want the grouped rule to carry real weight, the same weight the longhand selectors would have had. Swap :is() for :where() in the design-system snippet above and the default suddenly weighs as much as article h2, which may or may not lose to .featured h2 depending on the rest of the cascade. Pick deliberately.
Both use a forgiving selector list
A plain comma-separated selector list is all-or-nothing. If a single selector in it fails to parse, the browser throws out the entire rule. Write :hover, :unsupported-thing, :focus as a normal group and the whole block dies, including the :hover and :focus the browser understands perfectly well.
:is() and :where() accept a forgiving selector list: an unknown or invalid selector inside the parentheses is simply ignored, and the rest keep working.
/* one unsupported selector here kills nothing */
a:is(:hover, :focus, :some-future-pseudo) {
text-decoration: underline;
}The browser drops :some-future-pseudo, keeps :hover and :focus, and the rule applies. That is genuinely useful for progressive enhancement: you can list a newer selector alongside older ones and not lose the whole rule on browsers that have not shipped the new one yet. If you need to gate on real support rather than rely on forgiveness, reach for @supports feature queries and its selector() test.
One footnote so you are not surprised later: the forgiving behavior is specific to :is() and :where(). The newer :has() parent selector was deliberately spec'd to use a non-forgiving list, because an ignored argument inside :has() could silently change which elements match in a way that masks bugs. So "the selector list forgives" is a property of :is()/:where(), not a blanket rule for every functional pseudo-class.
Neither one matches pseudo-elements
This is the limitation that trips people up. :is() and :where() take a list of selectors, and pseudo-elements (::before, ::after, ::placeholder, ::first-line) are not valid inside that list. This does not work:
/* invalid: pseudo-elements are not allowed inside :is() */
p:is(::before, ::after) {
content: "x";
}The pseudo-element argument is invalid, the forgiving list drops it, and you are left matching nothing useful. If you want to group ::before and ::after, the comma list is still the tool:
.badge::before,
.badge::after {
content: "";
display: block;
}Pseudo-classes (:hover, :focus, :nth-child(), :not()) are fine inside :is()/:where(). Pseudo-elements (the double-colon ones) are not. Keep the two straight.
Heading-scoped styles, written once
A common real case: you want the same treatment for every heading level inside a region, but only inside that region.
article :is(h1, h2, h3, h4) {
font-family: var(--font-display);
line-height: 1.2;
text-wrap: balance;
}Without :is() that is four selectors (article h1, article h2, ...). And :is() nests, so you can compose scopes:
:is(article, aside) :is(h2, h3) {
margin-block-start: 1.5em;
}That matches an h2 or h3 inside either an article or an aside: four longhand combinations in one line. For a region whose styling should be trivially overridable (a CMS theme, a reset layer), make the outer scope a :where() so it contributes no specificity and authors can override per page without escalating.
You can pair this with a parent-aware selector, too: when you want the container styled based on what it holds, that is :has() territory, and when you want a parent styled because a child is focused, that is :focus-within. :is()/:where() are about grouping the selectors themselves, not reacting to descendants.
Browser support
Both :is() and :where() are Baseline and have been interoperable across Chrome, Edge, Firefox, and Safari since 2021. There is no prefix, no flag, and no polyfill needed in 2026; you can use them in production without a fallback. (The very old prefixed :-webkit-any() and :matches() experiments they descended from are long retired and should not appear in new code.)
FAQ
See also
- The CSS :has() parent selector: style an element based on what it contains, the companion functional pseudo-class (and the one with a non-forgiving list).
- CSS @supports feature queries: gate styles on real feature support when forgiving lists are not enough, including the
selector()test. - CSS :focus-within: style a parent when any child inside it has focus, the descendant-aware counterpart to selector grouping.
Sources
Authoritative references this article was fact-checked against.
- :is() CSS pseudo-class (MDN official docs)developer.mozilla.org
- :where() CSS pseudo-class (MDN official docs)developer.mozilla.org
- Selector list and forgiving selector lists (MDN official docs)developer.mozilla.org





