:has() is a relational pseudo-class that lets you style an element based on what it contains (or what follows it). It’s the closest thing we’ve got to a “parent selector,” and it unlocks practical patterns for forms, menus, cards, accordions, and more. Keep selectors scoped, mind specificity, and don’t benchmark with the universal selector unless you enjoy self-inflicted perf charts. See MDN for syntax/specificity details.
As an Amazon Associate I earn from qualifying purchases.
Why you (still) should care in 2025
- You can delete little JS sprinkles that only toggle classes.
- Your styles react to DOM state (required/invalid/focus) without wiring event listeners.
- It composes with
:is()/:where()/combinators, so you get expressive selectors with predictable specificity. MDN Web Docs
Crash course: syntax + specificity (30 seconds)
/* “Style .card if it HAS an <img> child” */
.card:has(img) { /* … */ }
/* Sibling/ordering tricks */
h2:has(+ h3) { margin-block-end: 0.25em; } /* if a subheading follows */
/* Specificity note: :has() takes the specificity of its most specific argument */
Specificity of
:has()equals the max specificity of what’s inside;:where()still has zero specificity, even if it wraps powerful stuff. Use:where(:has(...))to keep specificity tame. MDN Web Docs
If you’d like the spec language, see Selectors Level 4 “Relational pseudo-class: :has().” W3C
12 patterns you can ship today
1) Parent highlight on focus (goodbye focus-within hacks)
.field:has(:focus-visible) { outline: 2px solid Highlight; }2) Required/invalid form states—no JS
label:has(input:required)::after { content: " *"; color: #c00; }
form:has(:invalid) .submit { opacity: .5; pointer-events: none; }3) Card layout tweaks if media exists
.card:has(img) { grid-template-columns: 160px 1fr; }
.card:not(:has(img)) { grid-template-columns: 1fr; }4) Accordion with <details> that styles the container
.accordion:has(details[open]) { border-color: var(--accent); }
details:has(summary:focus-visible) { outline: 2px solid Highlight; }5) Menu items that only show carets when nested
nav li:has(> ul) > a::after { content: "▾"; }6) Error rows in tables
tr:has(td[aria-invalid="true"]), tr:has(td.error) { background: #fff6f6; }7) CTA reveal when a checkbox is on
.box:has(input[type="checkbox"]:checked) .cta { visibility: visible; }8) Progressive disclosure by input type/state
.payment:has(input[type="radio"][value="card"]:checked]) .card-form { display: grid; }9) Reduce heading gaps when another heading follows
h2:has(~ h2) { margin-block-end: .25em; } /* compact lists of headings */(This sibling trick is a classic MDN example in spirit—:has() anchored on the current element with a relative selector to what follows.) MDN Web Docs
10) Search results: badge parents with “new” items
.result:has(.is-new)::after { content: "New"; background: #ffe08a; }11) Sidebar only if it actually has content
.sidebar:not(:has(> *)) { display: none; } /* collapse the ghost chrome */12) State-driven theming
.theme-switcher:has([aria-checked="true"]) { --bg: #111; --fg: #eee; }Performance + maintainability (a tiny pep talk)
- Scope it. Anchor on a classed parent (e.g.,
.card:has(img)), not*:has(...). - Prefer direct combinators (
>,+,~) to deep descendant crawls. MDN Web Docs - Keep specificity predictable. Reach for
:where(:has(...))when you need power without weight. MDN Web Docs - Measure before complaining—your bottleneck is probably a 4MB hero image, not a three-token selector.
Browser support?
:has() is documented on MDN and defined in Selectors Level 4; it’s supported in modern evergreen browsers. Always check the MDN compatibility table for your target mix. MDN Web DocsW3C
Copy-paste snippets (grab bag)
Form gates
/* Disable submit until the form is valid */
form:has(:invalid) button[type="submit"] { opacity: .5; pointer-events: none; }Inline validation hint
.field:has(input:invalid) .hint { display: block; }Card grid auto-layout
.cards > .card:has(img) { grid-row: span 2; } /* taller cards if they have media */Low-specificity “power selector”
:where(.nav):has(.active) { box-shadow: inset 0 -2px var(--accent); }FAQ
Is :has() the long-awaited “parent selector”?
Pretty much. It’s broader: it can express relationships like “element that has a child/sibling matching…”, not just “style the parent.” WebKit
Will :has() wreck my performance?
Used sanely (scoped anchors, shallow relations), it’s fine. Don’t write *:has(...) unless you’re benchmarking bad ideas.
What’s the specificity trap again?:has() takes the specificity of its strongest argument. Wrap with :where() to zero it out when needed. MDN Web Docs
Can I chain it with :is()?
Yep. Combine for clarity, e.g. .card:has(:is(img, video)) to cover multiple media types. MDN has good examples.