CSS :has() Is No Longer Forgiving — Here’s What Changed

While updating the CSS Almanac entry for the :has() selector, a subtle but important detail came to light: :has() is no longer a “forgiving” selector. Previously, any invalid selectors inside its argument were ignored, and the rest of the list was evaluated normally. That behavior has been reversed, and the change has real implications for how you write selectors.

/* Example: Do not use! */
article:has(h2, ul, ::-scoobydoo) { }

The shift happened after the CSS Working Group received an issue report noting that forgiving behavior in :has() conflicts with jQuery when complex selectors like header h2 + p are used. As of a recent resolution, the entire selector list inside :has() is now invalid if any single selector in the list is invalid. Unlike :is() and :where(), which remain forgiving, :has() now follows strict parsing rules.

article:has(h2, ul) { }

The loss of forgiving behavior doesn’t leave you without options. Since :is() and :where() are still forgiving, you can nest either of them inside :has() to restore that flexibility:

article:has(:where(h2, ul, ::-scoobydoo)) { }

Which of the two you choose can matter in terms of specificity. :is() takes the specificity of its most specific argument, while :where() always contributes zero specificity. If you want to avoid inflating the specificity of your rule, :where() is the safer nesting choice.

/* Specificity: (0,0,1) */
article:has(:where(h2, ul, ::-scoobydoo)) { }

/* Specificity: (0,0,2) */
article:has(:is(h2, ul, ::-scoobydoo)) { }

Several articles and code examples across the web still reference the old forgiving behavior of :has(). If you maintain documentation or tutorials covering this selector, it’s worth a quick update to reflect that it is now unforgiving, and that the workaround is to wrap arguments in :is() or :where().