Overview

:has() is the relational selector: it matches an element based on what it contains or what follows it. It replaces scripts that watched children and toggled a class on the parent. It has been Baseline widely available since June 2026 (newly available December 2023), so it ships without fallbacks. The umbrella page is css.

Use :has() when state lives in a descendant

/* A form row with an invalid, touched input. */
.form-row:has(:user-invalid) > label { color: var(--color-danger); }
 
/* A card whose first child is an image gets edge-to-edge media. */
.card:has(> img:first-child) { padding-block-start: 0; }
 
/* Hide the sidebar while any dialog is open. */
body:has(dialog[open]) .sidebar { visibility: hidden; }

If a handler exists only to mirror child state onto a parent class, :has() replaces it.

Style form state with :user-invalid

Use :user-invalid and :user-valid rather than :invalid and :valid. They activate only after the user interacts, so a required field is not red on first paint. Both are Baseline widely available.

.form-row:has(input:user-invalid) { --color-border: var(--color-danger); }
.form-row:has(input:user-invalid) .error { display: block; }

Do not rely on color alone; pair it with an error message wired by aria-describedby. See forms.

React to siblings and layout

:has() takes any relative selector, including sibling combinators.

label:has(+ input:checked) { font-weight: 600; }
.app:has(> .side-panel) { grid-template-columns: 1fr 18rem; }

Test absence with :not(:has())

.card:not(:has(> img:first-child)) { padding-block-start: 1.5rem; }
.list:not(:has(li)) .empty-state { display: block; }

This covers state-by-absence cases that needed a JavaScript empty-state toggle.

Keep the argument specific

The browser re-evaluates :has() when relevant DOM changes. Keep the argument narrow (:has(> .thing) beats :has(*)) and avoid :has() on html or body for high-frequency changes such as scroll or pointer movement; use a state class set by a small script. Check the Performance panel only if style recalculation shows long frames.

Avoid common pitfalls

  • :invalid at first paint. A required input is invalid before interaction; use :user-invalid.
  • Specificity. :has() takes the specificity of its most specific argument, so a:has(#main) carries id weight. Layer your rules (css-cascade-layers).
  • Self-referential rules. A rule that styles a parent from a child that the same rule changes can loop; test interactive flows.
  • Hidden content and focus. visibility: hidden and display: none both remove content from the tab order; confirm keyboard users can still reach what they need (accessibility).