What :has() matches
:has(<selector>) matches an element when the inner selector
matches at least one element relative to it. Without a combinator,
the inner selector applies to descendants; combinators position
the match elsewhere:
article:has(img) { /* article that contains any image */ }
article:has(> img) { /* article whose direct child is an image */ }
li:has(+ li.active) { /* li immediately followed by .active li */ }
form:has(:invalid) { /* form with at least one invalid control */ }
:has(:focus-within) { /* any element whose subtree has focus */ }
The selector reached cross-engine support in late 2023 (Chromium 105 in 2022, Safari 15.4 in 2022, Firefox 121 in December 2023); caniuse data reports about 92% global support as of 2024.
Why it matters
For two decades, CSS lacked a parent selector. Authors worked
around the gap with class manipulation in JavaScript, which
required wiring up event listeners and synchronising classes
across components. :has() removes the JS dance for many
common cases.
Examples that previously required JS:
- Style a card differently when it contains an image.
- Style a navigation when one of its items is active.
- Style a form when it has any invalid control.
- Hide a label when its associated input is empty (
:has(:placeholder-shown)).
The selector is especially useful in design systems, where a component variant should respond to its content, not to a class the parent must remember to set.
Performance and the engine implementation
:has() is the first general selector that lets style depend on
descendants. Implementing it efficiently requires the engine to
invalidate styles upward when a descendant changes. The three
engines now use invalidation traversal — tracking which
ancestors might be affected by a DOM change — but the cost grows
with the depth of the matched tree.
Practical guidance from Una Kravets’s 2023 performance write-up:
- Anchor
:has()to a specific tag:article:has(img)is cheaper than:has(img)(latter scans every element). - Avoid deeply nested traversals:
:has(.foo .bar .baz)is expensive. Prefer narrower descendants. - Prefer descendant combinator over universal traversal where possible.
- Avoid
:has()on the document root; restrict to component scopes.
Microbenchmark numbers from the engine teams suggest a
well-anchored :has() adds about 1–3% to selector matching
cost; a broad un-anchored one can add 20% or more.
Common patterns
/* Card variant when sponsor banner present */
.card:has(.sponsor-banner) {
border-color: var(--c-warning-fg);
}
/* Form validation hint */
.form:has(:invalid) .submit { opacity: 0.5; pointer-events: none; }
/* Empty-state message when list is empty */
.todos:has(li) .empty-state { display: none; }
/* Adjacent sibling: previous element of an active marker */
.step:has(+ .step.current) {
/* the step right before the current one */
}
The form-validation pattern is the most cited use; it removed the need for JavaScript-driven submit-disable in about 80% of greenfield applications surveyed in Smashing Magazine’s 2024 CSS report.
Restrictions
:has() cannot contain another :has(). Pseudo-elements are
not allowed inside. The CSS Selectors Level 4 grammar defines
the relational selector (§17.1);
all engines comply.
Common pitfalls
- Specificity surprises.
.card:has(.x)has the specificity of.cardplus.x. Authors expecting class-only specificity see unexpected wins. :has()plus animation. Element added or removed inside the:has()triggers style invalidation; animations may flicker unlesstransitionis restricted to specific properties.:has()plus print. Some print engines (Prince XML, WeasyPrint) implement:has()partially; check the target.:not(:has(...))is permitted but very expensive; the engine must evaluate both negation and relational matching.
Cross-engine support
Interop pass-rate on the Selectors L4 relational subset
is about 96% across Chromium (Blink), WebKit, and Gecko per the
2024 dashboard. Edge cases involving nested :has() (forbidden)
or pseudo-elements (forbidden) account for most remaining gaps.
Further reading
- CSS Selectors Level 4, §17.1 Relational selector.
- Una Kravets,
:has()performance and patterns. - Bramus’s
:has()use-case catalogue is the densest single-page reference. - Web Platform Tests for relational selectors live at wpt.fyi/css/selectors/scoping.