Skip to content
Craft & CSS

:has() is finally useful

The parent selector that CSS refused us for twenty years shipped, and then the internet used it to write card hacks. Three :has() patterns that survive production, plus the performance reflex that keeps them cheap.

Advanced2 min read (computed · recorded 9)updated 2026-09-10csshasselectorsmodern css

What :has() actually gives you

:has() lets a selector test its own descendants and siblings — the parent selector, the previous-sibling selector, the container-aware selector. .card:has(img) matches a card only if it contains an image. That single capability collapses dozens of JavaScript class-toggling rituals into declarative CSS: form states, card variants, layout adjustments driven by content.

the-three-patterns.css
/* 1 — container adapts to its content */
.stats-grid > .panel:has(.sparkline) { grid-column: span 2; }

/* 2 — state travels to the parent */
.field:has(input:user-invalid) {
  border-color: var(--danger);
}
.field:has(input:focus-visible) {
  box-shadow: 0 0 0 3px var(--focus-ring);
}

/* 3 — sibling reacts to sibling */
.tabs > .tab:has(:checked) {
  color: var(--ink);
  box-shadow: inset 0 -2px 0 var(--accent);
}

The patterns that earn their keep

  • Form validation without JS: .field:has(:user-invalid) styles the whole field group when its input fails — the message, the border, the icon, all in one rule, updated live by the browser.
  • Content-driven layout: a panel that widens when it contains a table, a card that drops its media row when the media is missing — layout responds to what is actually in the box.
  • Focus-within that means it: :has(:focus-visible) reaches every descendant, not just direct children — the container glows when any control inside it is focused.
  • The navigation current-state: a nav item that styles itself when it contains the current link — no server-side class to keep in sync.
The performance reflex

:has() is powerful and the browser has to work for it — a :has() selector in a hot path (a selector applied to thousands of elements, or one that queries deep into a huge DOM) can cost real time. The reflex: scope it tightly (start from a class, not *) and keep the inner selector shallow. If a page has 5,000 rows each running :has(), measure before you celebrate.

Where it stays a party trick

The famous :has() card hacks — counting stars, styling the third child — are fun and useless. The pattern that matters is state that lives in the DOM (checked, invalid, focused, present) expressing itself on ancestors. If you find yourself toggling a class in JavaScript because 'the parent needs to know', stop and ask whether :has() already knows.

Practise the lesson

Theory sticks when you ship it. These original Motif assets put this guide's lesson to work — open one and copy it into your own page.