skip to content

In CSS, what happens to a rule when one selector inside :is() is unknown to the browser, and how does that differ from a plain comma-separated selector list?

level: middleimportance: nice to knowfreq 26%

answer

  1. default is all-or-nothing per rule
  2. one bad selector, whole rule discarded
  3. two functions parse argument by argument
  4. negation cannot afford to drop an argument
  5. feature detection needs the strict behaviour

basics

~20 s

:is() and :where() take forgiving selector lists: an unparseable argument is dropped and the rest of the selector still applies. In an ordinary comma-separated selector list, one invalid selector invalidates the entire rule, declarations included.

solid answer

~40 s

CSS error handling for selectors is all-or-nothing by default: if any selector in a comma list fails to parse, the whole rule is discarded, so one vendor-specific selector can silently kill styles for every other browser. `:is()` and `:where()` change that — they accept a **forgiving selector list**, where arguments the engine cannot parse are simply dropped and the remaining ones keep matching. So `p:is(.intro, :some-unknown-thing)` still styles `p.intro` everywhere. That makes `:is()` a clean way to include an engine-specific selector alongside the standard one, as in `input:is(:autofill, :-webkit-autofill)`. Two exceptions matter: `:not()` and `:has()` are **not** forgiving — an unknown argument there invalidates the selector, which is deliberate so `@supports selector(:has(a))` reports something meaningful.

code

css · 8 lines
css
.intro,
:some-unknown-thing {
  color: rebeccapurple;
}

p:is(.intro, :some-unknown-thing) {
  color: rebeccapurple;
}

go deeper

for a junior

Know the baseline rule: in a comma-separated selector list, one selector the browser cannot parse discards the whole rule, declarations included.

for a middle

Explain that :is() and :where() take forgiving selector lists that drop unparseable arguments individually, and that :not() and :has() are strict by design.

for a senior

Show how you use this in shipping code — grouping an engine-specific selector inside :is() instead of a comma list, while still gating real enhancements behind @supports selector() so the fallback is explicit.

for a principal

Own the convention for how the codebase handles uneven selector support: where silent degradation is acceptable and where a feature query with a written baseline is required before a selector enters shared CSS.

## The default rule: one bad selector kills the rule CSS parsers are built to skip what they do not understand, but the granularity matters. For a selector list, the unit is the **whole rule**. If any selector in a comma-separated list fails to parse, the entire rule — every selector in the list and every declaration in the block — is thrown away: ```css /* If the engine does not understand the second selector, .intro loses its colour too. */ .intro, :some-unknown-thing { color: rebeccapurple; } ``` This is why grouping a vendor-specific selector with a standard one has always been a bug. The classic historical example is placeholder styling, where authors grouped several engine-specific selectors into one list and every browser dropped the rule because it recognised only its own. ## Forgiving selector lists `:is()` and `:where()` are defined to take a **forgiving selector list**. Parsing works per argument: anything the engine cannot handle is discarded, the rest is kept, and the rule survives. ```css p:is(.intro, :some-unknown-thing) { color: rebeccapurple; } /* Behaves as p:is(.intro) in an engine that does not know the second argument. */ ``` A useful consequence: you can pair a standardised selector with an engine-prefixed one that means the same thing and let each browser take whichever it recognises: ```css input:is(:autofill, :-webkit-autofill) { box-shadow: 0 0 0 100px canvas inset; } ``` Both of those are real pseudo-classes — `:autofill` is the standard one and `:-webkit-autofill` the older WebKit form — and the forgiving list means neither one's absence breaks the other. ## The exceptions: :not() and :has() The forgiving behaviour is a property of the specific function, not of functional pseudo-classes in general. `:not()` is **not** forgiving. `p:not(.a, :unknown)` is invalid as a whole. That is semantically necessary: dropping an argument from a negation would silently *widen* what the rule matches, styling elements the author explicitly excluded — a much worse failure than not applying at all. `:has()` is **not** forgiving either. Its list was originally specified as forgiving and the CSS Working Group changed it, precisely so that feature detection would work: `@supports selector(:has(a))` can only give a truthful answer if an engine without `:has()` treats the selector as invalid rather than quietly accepting it. ## How to use this deliberately Three practical habits follow. First, when you must group a selector that only some engines know, wrap the group in `:is()` rather than using a comma list. The cost is that `:is()` also imposes its most-specific-argument weight on the group, so keep the arguments comparable in specificity. Second, do not rely on forgiving parsing as a substitute for feature detection. A dropped argument is silent — nothing appears in devtools to say a selector was thrown away — so a genuinely conditional enhancement belongs in `@supports selector(…)`, where the intent is explicit and reviewable. Third, remember that forgiving parsing covers *selectors*, not *declarations*. Declaration-level error handling is separate and already per-declaration: an unknown property or an invalid value drops just that line, leaving the rest of the block intact. The two mechanisms are often conflated in interviews, and being able to separate them cleanly is the mark of someone who has actually read the error-handling rules. ## Why interviewers ask it It is a compact test of whether a candidate understands CSS as a specified language with defined error handling rather than as a bag of properties. The follow-up is almost always "so why isn't `:not()` forgiving too" — and the answer, that dropping a negated argument would broaden the match set instead of narrowing it, shows you can reason about the spec's intent, not just recall the rule.

  • Why is :not() deliberately not forgiving?
    Because dropping an argument from a negation widens the match set. If `p:not(.a, :unknown)` silently became `p:not(.a)`, elements the author explicitly excluded would start receiving the styles. Failing loudly — invalidating the selector — is the safer outcome, so `:not()` requires every argument to parse.
  • Does forgiving parsing apply to declarations as well as selectors?
    No, they are separate mechanisms. Declaration error handling has always been per declaration: an unknown property or invalid value drops that one line and the rest of the block still applies. Forgiving selector lists extend similar granularity to selectors, but only inside `:is()` and `:where()`.
  • If forgiving lists exist, why still use @supports selector()?
    Because a dropped argument is silent — nothing signals that a selector was discarded, and you cannot provide an alternative. `@supports selector(:has(a))` makes the condition explicit and lets you ship a baseline plus an enhancement. It also depends on `:has()` being non-forgiving, which is exactly why that list is strict.

saying these in an interview costs you the question

  • Assuming an invalid selector only drops itself from a comma list
  • Thinking every functional pseudo-class parses forgivingly
  • Believing :has() ignores arguments it cannot parse
  • Confusing per-declaration error handling with selector error handling
  • Treating forgiving parsing as a substitute for @supports

context