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
:invalidat first paint. A required input is invalid before interaction; use:user-invalid.- Specificity.
:has()takes the specificity of its most specific argument, soa: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: hiddenanddisplay: noneboth remove content from the tab order; confirm keyboard users can still reach what they need (accessibility).