The parent selector · previous siblings · quantity queries — zero JavaScript
Toggle content in and out of the card below. The card itself restyles — border, padding, glow — purely from CSS rules that ask "do I contain X?" No classes are added to the card, no JavaScript touches its styles.
Watch my border and shadow change as content appears inside me. The JS only toggles classes on my children — my own styling reacts via :has().
Errors appear only after you've typed and left the field — the polite validation UX that used to need JavaScript.
CSS has next-sibling (+) and later-siblings (~) combinators — but nothing selects backwards. :has() fixes that. Hover the boxes: the one before your cursor reacts.
The grid below changes its border color — and even its column count — based on how many items it contains. Add and remove items to see each rule fire.
Production patterns that used to require JavaScript — plus the four rules that trip people up.
The checkbox state drives the entire theme via :has() — no JS event listener anywhere.
Pair with light-dark() and every themed value flips from one checkbox. JS is only needed to pre-check the box from saved preference.
The spotlight effect every product grid wants — one rule, no mouseenter/mouseleave listeners.
The scroll-lock JavaScript that every modal library ships — replaced by one selector watching for an open dialog.
Order matters: .card:not(:has(img)) means "card without images". .card:has(img:not([alt])) means "card containing an image that lacks alt". Different questions.
An ID inside :has() silently makes the whole rule ID-strength — a classic "why won't my override apply" mystery. Wrap arguments in :where() when you want zero added weight.
Performance: keep the anchor specific (.card:has(img), never div:has(img)) and constrain the argument with > or + — the browser then checks direct children instead of traversing whole subtrees on every DOM change.
:has() was voted the most-used and most-loved CSS feature in the State of CSS 2025 survey. Gate enhancements with @supports selector(:has(*)) where the fallback matters.