Contents

Frontend Development › CSS

:has() Selector

Selecting a parent based on its children.

Also known as: :has selector, has pseudo-class, parent selector

:has() selects an element based on what’s inside it — the long-missing “parent selector.” form:has(input:invalid) styles the form when any field is invalid; figure:has(figcaption) adjusts figures with captions; .card:has(img) lays out differently when there’s an image.

/* the form itself reacts to a child's state */
form:has(input:invalid) { border-color: red; }
.card:has(> img) { grid-template-columns: 120px 1fr; }

It also takes state pseudo-classes, sibling combinators and quantity queries (li:has(+ li + li) — lists with 3+ items), collapsing whole JavaScript class-toggling routines into one selector.

The classic mistakes:

  • Performance blindness. :has() can force the browser to re-evaluate up the tree on DOM changes; broad :has() over huge subtrees on frequently-mutating pages costs. Scope it tightly.
  • Forgetting support floors. Extremely old browsers lack it; if your baseline includes them, provide a no-:has() fallback rather than silent restyling.
  • Over-nesting with it. Combining :has() with deep descendant chains multiplies matching cost. Keep selectors short and close.
  • Using it where markup would do. A class toggled server-side or at render is cheaper and clearer than a clever :has() — reach for the selector when state is genuinely dynamic, not as decoration.
  • Quantity-query fragility. Sibling-counting selectors break the moment markup order shifts. Prefer explicit classes for load-bearing variants.
  • Assuming style containment. :has() doesn’t isolate — matched parents still cascade normally. Encapsulation needs shadow DOM or scope, not relational selectors.
  • Testing only the positive. :has() restyles on child removal too; dynamic lists adding/removing nodes flip matches constantly. Verify both directions.

How to use it: relational styling for genuinely child-dependent presentation — validity, presence, counts — scoped narrowly, with fallbacks at the support floor. It’s the selector that retires a generation of JS class toggles.