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.
/* 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.
: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.