The :has() selector
The :has() relational selector styles an element based on what it contains — the long-wished-for parent selector — replacing a lot of state-tracking JavaScript.
In one line: :has() styles an element by what lives inside (or beside) it — the parent selector CSS lacked for twenty years — so a card can highlight because it holds a checked box without a single line of JavaScript.
What it is
:has() is a relational pseudo-class. Every other selector reads top-down — you match an element by its own tag, class, or state. :has() reads the other way: it matches an element because of what it contains or what follows it. .card:has(:checked) selects the card that holds a checked control; label:has(+ input:focus) selects a label whose adjacent input is focused.
That is the long-requested "parent selector," and a good deal more. The argument inside the parentheses is a full selector — combinators and all — evaluated relative to the element you are matching. So :has() lets you style up the tree (a container based on a descendant) and sideways (an element based on a sibling), not just down.
Why it matters
A large share of UI JavaScript exists only to toggle a class on an ancestor: this input is invalid, so add .has-error to its form row; this box is checked, so mark the card .is-selected. The state already lives in the DOM — :has() simply lets CSS read it, and that whole category of event-listener bookkeeping disappears.
Because it is native, the work happens in the engine's selector matching, not in a re-render. No effect to wire up, no class to keep in sync, no stale state when the DOM changes out from under you. You describe the relationship once in a stylesheet and the browser keeps it true. As of 2026 :has() ships in every current browser, so it is a baseline tool, not a progressive nicety.
See it
Tweak it3
Switch the scenario and toggle the child state — check the box, type an invalid email, clear the field. Watch the parent element respond: the card highlights, the row turns to an error state, the hint appears and vanishes. Each panel also prints the equivalent :has() rule so you can map the behavior to the selector that drives it.
How it works
The syntax is target:has(<relative-selector>). The selector in the parentheses is evaluated relative to target, so the combinator at its front decides the relationship:
:has(:checked)—targetcontains a checked descendant (descendant combinator is implied).:has(> img)—targethas a direct-childimg.:has(+ .error)—targetis immediately followed by an.errorsibling.
The argument is a forgiving selector list: :has(.a, :unsupported, .b) keeps matching on .a or .b even if one entry is invalid or unknown, so adding a future-proofed selector never breaks the rule. :has() reacts to real, browser-tracked states — :checked, :invalid, :focus, :placeholder-shown, structural matches. It does not react to ARIA or arbitrary properties you set in JavaScript unless those map to an actual selector-matchable state, which is exactly why pairing the visual change with real semantics (below) matters.
Build it
A form row that flips to an error state when it contains an invalid field, and a card that highlights when it contains a checked box — both with zero JavaScript.
/* The ROW restyles because it CONTAINS an invalid input. */
.row:has(input:user-invalid) {
border-color: var(--danger);
background: var(--danger-soft);
}
.row:has(input:user-invalid) .error-message {
display: block; /* reveal the message only when the row is in error */
}
/* The CARD restyles because it CONTAINS a checked control. */
.card:has(:checked) {
border-color: var(--accent);
background: var(--accent-soft);
}
/* A hint shown only while the field is still empty. */
.field:has(input:placeholder-shown) .hint {
display: block;
}Make it yours
Use the controls beside the demo above to change :has() scenario, pre-fill the input, and show the :has() rule — each change updates the example live.
Experiment in the playground- Start on checked and toggle the box: the whole card — the parent — changes, not just the input. That is the relationship
:has()expresses in one rule instead of a change handler. - Switch to invalid and type a few characters without an
@: the row enters its error state the moment the contained input is invalid, and clears itself when the value becomes valid or empty. - On empty, type into and then clear the field — the hint follows
:placeholder-shown, appearing only while there is nothing to read.
Reproduce it with an LLM
Reproduce it with an LLM
You are a senior front-end engineer. Use the CSS `:has()` relational selector to style a form WITHOUT JavaScript: (1) a label/field row that turns into an error state when it `:has(:invalid)`, (2) a card that highlights when it `:has(:checked)` (a selected option), and (3) a 'gift options' section revealed only when a checkbox is checked, via `:has()` on an ancestor. Keep everything keyboard-accessible with visible focus-visible styles, and make sure the state is also conveyed by something other than color (text or icon), not color alone. Return only the HTML and CSS.
Pitfalls & accessibility
- Watch performance on broad targets: a rule like
:has(...)on a high-frequency or document-wide selector forces the engine to re-check relationships on DOM mutations. Scope the target tightly (a specific component class), keep the argument simple, and you will not notice the cost.