:has() is the parent selector. .plan:has(:checked) styles the whole card when its own radio is checked. For twenty years CSS only looked down and forward; now it looks up. Every major browser supports it since 2023.
Validation without a handler: .field:has(:user-invalid) turns the border, label and icon at once. No onChange, no error state in React. The browser already knows the email is wrong, so let it drive the styling.
Escalate to the form: form:has(:user-invalid) button { opacity: .4 } greys out the submit button while any field is invalid. One rule, zero derived state.
Layout follows the DOM: .app:has(aside) { grid-template-columns: 280px 1fr } grows a column when a sidebar renders and collapses when it is removed. No showSidebar prop to thread through.
Quantity queries live in CSS: .grid:has(> :nth-child(n + 5)) tightens gap, padding and font size across all cards the moment a fifth one arrives. No counting items in JavaScript.
The document reacts to a modal: body:has(dialog[open]) { overflow: hidden } locks scroll and a second rule dims the app behind it. Close the dialog and everything reverts on its own. No cleanup effect.
Do / Don't
Do: Reach for :has() when the state already lives in the DOM: checked, open, invalid, child count.
Do: Use :user-invalid instead of :invalid so errors show after the user leaves the field, not on first render.
Do: Put the rule on the container so border, label and icon all update from one selector.
Don't: Mirror DOM state into React state just to toggle a class the browser can already compute.
Don't: Write a cleanup effect for scroll lock or dimming that a body:has(dialog[open]) rule undoes by itself.
Don't: Count children in JavaScript to pick a layout when a quantity query does it in one rule.
The Design System Blueprint
An 11-page visual guide to consistent, scalable UI: colors, typography, spacing, components, motion.