What does the CSS selector `.card :is(h2, h3)` match, and what would you write instead if :is() were not available?
answer
- one selector instead of a comma list
- shared prefix written once
- matches any argument in the list
- distributes over the surrounding selector
- expands to .card h2, .card h3
basics
~20 sIt matches any h2 or h3 that is a descendant of an element with class card — the same elements as the longhand list .card h2, .card h3. :is() matches an element if it matches any selector in its argument list, so the shared prefix is written once.
solid answer
~40 s`:is()` is the matches-any pseudo-class: it matches an element when that element matches at least one selector in its argument list. So `.card :is(h2, h3)` matches an `h2` or an `h3` anywhere inside a `.card`, which is the same match set as writing `.card h2, .card h3`. The value is that the surrounding parts of the selector are written once and distribute over every argument — with two `:is()` calls in one selector the longhand expansion multiplies out, so `:is(article, section) > :is(h2, h3)` replaces four selectors. Arguments can be full complex selectors like `:is(.a > .b)`, but pseudo-elements are not allowed inside `:is()`. One caveat: the expansion is equivalent for *matching*, not always for specificity, because `:is()` takes the weight of its most specific argument.
code
css · 9 lines.card :is(h2, h3) {
margin-block-start: 0;
}
/* same match set, written longhand */
.card h2,
.card h3 {
margin-block-start: 0;
}go deeper
Be able to read :is(a, b) aloud as "matches either a or b" and write out the longhand selector list it replaces. Knowing that the surrounding parts of the selector are written once is enough here.
Explain that :is() distributes over the rest of the selector and that its specificity is that of its most specific argument, so the longhand expansion is match-equivalent but not always weight-equivalent.
Show judgment about what belongs in one :is(): keep the argument weights similar, avoid mixing an ID with type selectors, and reach for :where() when the group should stay overridable.
Own the convention for how selector grouping is used across a codebase — where grouped selectors improve readability versus where they hide which elements a rule really touches during review and refactoring.
## What :is() is `:is()` is a functional pseudo-class defined in CSS Selectors Level 4, sometimes called the *matches-any* pseudo-class. It takes a comma-separated list of selectors and matches an element if that element matches **any** one of them. It is a selector, not a preprocessor feature: the browser evaluates it directly, and no build step is involved. ## Reading `.card :is(h2, h3)` Read it left to right like any other compound/complex selector. `.card` matches an element with the class `card`; the whitespace is the descendant combinator; `:is(h2, h3)` matches an element that is either an `h2` or an `h3`. Put together: *any h2 or h3 at any depth inside a `.card`*. Without `:is()` you would write the two selectors out as a selector list: ```css .card h2, .card h3 { margin-block-start: 0; } ``` ## Why it is useful: the prefix is written once The real payoff appears when the shared part is long or when several groups combine. `:is()` distributes over the rest of the selector, and multiple `:is()` calls multiply: ```css :is(article, section) > :is(h2, h3) { line-height: 1.2; } /* longhand equivalent — four selectors */ article > h2, article > h3, section > h2, section > h3 { line-height: 1.2; } ``` That combinatorial growth is exactly the maintenance problem `:is()` removes: adding one more container to the first group used to mean adding two more lines, and now it means adding one word. ## What can go inside Arguments may be complex selectors, including combinators — `:is(.sidebar > .widget, .footer .widget)` is valid. What may **not** appear inside `:is()` is a pseudo-element: `:is(::before, ::after)` does not work, and pseudo-elements have to be attached outside the function instead. `:is()` itself can appear anywhere a simple selector can, including in the middle of a compound: `a:is(.primary, .cta):hover`. ## Two behaviours that are not obvious from the expansion The longhand expansion is a good mental model for *which elements match*, but two things do not carry over. First, **specificity**. A selector list is evaluated per selector, so `.card h2` and `.card h3` each score their own weight. `:is()` instead takes the specificity of its **most specific argument**, applied uniformly. With `:is(h2, h3)` both arguments are type selectors, so nothing changes. But `:is(h2, #title)` scores as an ID for every match, even when it actually matched a plain `h2`. Mixing wildly different weights inside one `:is()` is therefore a trap; `:where()` exists for the opposite case, where you want zero weight. Second, **error handling**. In an ordinary comma-separated selector list, one invalid selector invalidates the entire rule. `:is()` takes a *forgiving* selector list: an argument the browser cannot parse is dropped and the rest of the selector keeps working. ## A practical shape A very common use is heading and flow rhythm, where the same declaration applies to a family of elements inside a scoped region: ```css .prose :is(h1, h2, h3, h4) { text-wrap: balance; } .prose :is(ul, ol) { padding-inline-start: 1.5rem; } ``` ## Support `:is()` is supported in every current major browser and has been for years, so it needs no fallback in new work. If you must support very old engines, the safe fallback is simply the longhand selector list it expands to.
- Can you put a pseudo-element inside :is()?No. `:is()` accepts complex selectors but not pseudo-elements, so `:is(::before, ::after)` is invalid. Attach the pseudo-element outside the function instead — `:is(.card, .panel)::before` is the correct shape, and it applies the pseudo-element to whichever of the two matched.
- Is `.card :is(h2, h3)` always interchangeable with `.card h2, .card h3`?For matching, yes. For specificity, only when the arguments carry equal weight. `:is()` takes the weight of its most specific argument, so `:is(h2, #title)` scores as an ID even when it matched a plain `h2`, while the longhand list would score each selector separately.
saying these in an interview costs you the question
- Calling :is() a Sass mixin or build-time shorthand
- Assuming :is() matches all of its arguments at once
- Believing :is() always has zero specificity
- Putting ::before or ::after inside :is()
- Thinking :is() styles the parent rather than the matched element