Limitations of the :has() Selector

To better understand what :has() can do, let’s quickly review the most impactful things it can’t do.

:has() isn’t a forgiving selector

Unlike the other relational pseudo-class selectors :is() and :where(), selectors within :has() must all be considered valid by the browser for the rule to be applied. This is because :has() is not a “forgiving selector”. This means that, given :has(:invalid-selector, img, p) , the whole rule will be thrown out and not applied—even if there is an <img> or a <p> present.

:has() can’t be nested within another :has()

Unfortunately, a selector like :has(p:has(a)) is invalid because :has() isn’t allowed to be nested within another :has() .
The good news is that, because of how the relational selector list works, we can write :has(p a) and achieve the same result—that is, testing whether a paragraph containing an <a> element exists.

:has() can’t detect the presence of pseudo-elements

While :has() can be used to detect pseudo-classes like :checked , it can’t check for pseudo-elements due to performance implications. The spec notes that, since pseudo-elements only exist conditionally and must be attached to their ancestors, querying for pseudo-elements with :has() introduces cycles, given how the browser evaluates CSS rules. Examples of pseudo-elements include:

  • ::before

  • ::after

  • ::marker

  • ::selection

Get hands-on with 1200+ tech skills courses.