CSS :is() and :where(): Grouping Selectors Explained
:is() and :where() group selector lists into one rule. Same matching logic, different specificity — here's when to reach for each.
The :is() and :where() pseudo-classes let you group a list of selectors into a single rule instead of repeating a common ancestor or descendant chain over and over. They match exactly the same elements — the only difference is specificity, and that difference is the whole reason you’d pick one over the other.
The problem they solve
Without grouping, targeting the same descendant across several possible parents means writing the full selector out for each one:
header h1, header h2, header h3,
main h1, main h2, main h3,
footer h1, footer h2, footer h3 {
font-weight: 600;
}
That’s nine selectors to express one idea: “any heading inside header, main, or footer.” :is() collapses it:
:is(header, main, footer) :is(h1, h2, h3) {
font-weight: 600;
}
Same result, far less repetition, and it’s easier to keep in sync when a new section gets added later.
How :is() works
:is() takes a comma-separated selector list and matches an element if any selector in the list matches. It behaves like a wildcard for the group. A common pattern is trimming a component’s own root-level margin:
.card :is(p, ul, ol):last-child {
margin-bottom: 0;
}
Nesting works too — :is() can appear inside another :is(), and it composes with combinators (>, +, ~) the same way a plain selector would. This is the same forgiving-list behavior that shows up in the :has() selector, which is worth reading alongside this one since both were part of the same wave of selector-level CSS additions.
The specificity twist: :where()
Here’s the catch with :is(): its specificity is the highest-specificity selector in its argument list. So :is(#sidebar, .card) p carries the specificity of an ID selector, even though it also matches .card p. That can quietly make a rule harder to override later — exactly the kind of surprise covered in how CSS specificity actually works.
:where() solves this by always resolving to zero specificity, regardless of what’s inside it:
:where(header, main, footer) :where(h1, h2, h3) {
font-weight: 600;
}
That rule now has specificity 0,0,0 — a single class selector anywhere else in your stylesheet will beat it without needing !important or a more specific selector chain. This makes :where() the natural tool for resets, base typography, and any styling you expect components to override freely.
:is() vs :where()
:is() | :where() | |
|---|---|---|
| Matching logic | Identical | Identical |
| Specificity | Highest-specificity argument | Always zero |
| Best for | Grouping selectors you still want to “count” | Resets, defaults, low-priority base styles |
| Overriding later | Requires equal or higher specificity | A single class overrides it |
| Invalid selector in list | Whole list is forgiving (invalid entries ignored) | Same forgiving behavior |
Both accept a forgiving selector list — if one selector in the group is invalid or unsupported, the browser drops just that one and still matches the rest, rather than throwing out the entire rule the way a plain comma-separated selector list would with an unsupported entry.
Practical use cases
Component resets that stay overridable. A design system baseline is exactly what :where() is for — you want the defaults applied, but you don’t want them to ever outrank a component’s own class:
:where(button, input, select) {
font: inherit;
color: inherit;
}
Scoping nested structures without a mess of combinators. Combined with :has() or container queries, :is() keeps compound conditions readable instead of turning into a wall of comma-separated duplicates.
Working alongside cascade layers. If you’re already using @layer to control override order, :where()’s zero specificity is a second, complementary tool — layers control which block wins, :where() controls specificity within a block. Using both together keeps a reset layer from ever needing !important.
Native nesting. CSS nesting actually desugars to :is() under the hood in several cases — when you nest a selector without a leading combinator, the browser treats the parent reference as if it were wrapped in :is(). Understanding :is()’s specificity behavior explains some nesting specificity surprises before they happen.
Things to watch for
:is()specificity can bite you later. If you write:is(.a, #b), every match through that rule carries ID-level specificity, even for elements that only matched.a. If you don’t need that weight, use:where().- Pseudo-elements aren’t allowed inside either.
:is(::before, ::after)is invalid — both are pseudo-class functions and only accept simple/compound selectors, not pseudo-elements. - They don’t change what matches, only how strongly it wins. If two rules using
:is()and:where()target the same element with conflicting properties, source order and specificity resolve it exactly as they would for any other selectors — there’s no special-casing beyond the specificity value itself.
The takeaway
:is() and :where() both group selector lists and match identically — the choice comes down to specificity. Reach for :is() when you want the grouped selector to carry real weight in the cascade, and :where() when you’re writing something meant to be overridden, like resets or base component styles. Used together with cascade layers and nesting, they cut a lot of repetitive selector-list boilerplate out of a stylesheet without changing what actually renders.
Tagged
Keep reading
Takina · · 4 min read prefers-reduced-motion: Building Accessible CSS Animation
The prefers-reduced-motion media query detects a user's OS-level motion setting so CSS animations can be toned down or removed for people who need it.
Takina · · 4 min read CSS object-fit and object-position, Explained
object-fit controls how an image or video is cropped inside its box, and object-position controls which part of it stays visible. How they work together.
Takina · · 5 min read CSS inherit, initial, unset & revert Explained
CSS's four global keywords control where a property's value comes from. How inherit, initial, unset, and revert differ, with a comparison table.