":has() didn't add a feature. It removed an entire category of JavaScript."
The Pattern Shift
For two decades, the answer to "style a parent based on what's inside it" was: write JavaScript that watches the children, toggles a class on the parent, and styles the class. Every form with a "submit disabled until valid" indicator did this. Every layout with "hide aside when main is empty" did this. Every card that lit up when an interior link was hovered did this.
:has() made all of that obsolete.
The Five High-Value Patterns
1. Form-Wide Validation Visualization
Indicate that a form has problems without writing per-field error handling. The form itself takes the highlighted state:
2. Sidebar / Pane Adaptation
Hide a sidebar when there's no main content, or show one when content meets a criterion:
3. Interactive Card Highlighting
Highlight a card when a child link or button is hovered or focused, without JavaScript:
4. Conditional Decoration Based On Descendant
Add a marker to elements based on what they contain:
5. Layout Variation Based On Slotted Content
A grid layout that adapts based on whether a featured item is present:
Sibling :has Patterns
:has() can use any combinator inside, not just descendants. Sibling-aware patterns:
li:has(+ li:hover)— the item just before the hovered one (style preceding item).li:has(~ li.active)— items before the active one.label:has(+ input:invalid)— labels whose adjacent input is invalid.
The Performance Concern (And Why It's Mostly Moot)
Early skepticism about :has() performance was real: a naive implementation would re-evaluate every parent's :has match on every DOM change. Modern browsers (2023+) optimize this with selector caching and invalidation tracking. For typical applications, :has() performance is indistinguishable from other selectors. Only deeply-nested :has() with complex inner selectors on very large DOMs causes measurable overhead — and there, the right answer is to simplify the inner selector, not avoid :has().
What :has() Doesn't Do
- Doesn't watch dynamic content changes that aren't in the DOM. If a child's intrinsic state is JavaScript-only (a React state, a Web Component internal),
:has()can't see it. The state has to surface as a DOM attribute or CSS class for:has()to query. - Doesn't replace event handlers.
:has(:hover)is a CSS state; it doesn't fire JS events. If you need to run code when a child is hovered, you still need an event listener. - Doesn't work across shadow DOM boundaries.
:has()inside a parent can't see inside a custom element's shadow tree. Use parts/slots or expose state via attributes.
:has() can replace it. Form validation styles, layout adaptation, hover propagation, content-aware decoration — most are now pure CSS. Less code, fewer bugs, faster render.Browser Support
Baseline 2023 across Chrome, Edge, Safari, Firefox. :has() is production-safe in 2026. Caniuse confirms — no polyfill needed.
Pippa's Note
.chat-row:has(.attachment) .badge { display: inline; }. The Council UI used to dim the picker via React state when no brain was selected; now it's .council-panel:has(.brain-select:invalid) .picker { opacity: 0.5; }. A whole layer of UI state that used to live in React now lives in CSS. The components got simpler, the state had fewer places to drift out of sync, and the renders got faster.