CSS :has() Selector Table

PatternSelectsField note
div:has(img)Div containing an imgThe parent selector - CSS's most-requested feature, shipped 2023
label:has(+ input:invalid)Label before invalid inputSibling scan AFTER the label - form validation styling
li:has(ul)List item containing sublistExpandable-tree styling without JS classes
figure:has(figcaption)Figure with captionStyle captioned vs bare figures differently
card:has(.badge)Card containing a badgeE-commerce: style products with sale badges
form:has(input:required) buttonSubmit in forms with required fieldsScope button styling to required forms
a:not(:has(img))Text-only linksThe not() composition: links without images
Reference: the MDN :has() reference. :has() is the PARENT selector - CSS could not look upward for 25 years, and :has() ended that in every major browser by 2023. The performance caveat: :has() invalidates upward, so a change inside a matched child restyles the parent and all its children - fine for forms and cards, expensive on huge lists. Bottom line: :has() replaces the JS class-toggle pattern (add .has-image to the parent when it contains an img) with pure CSS - the wrapper-div-for-styling era is over. Related tools: CSS selectors table, pseudo-classes table, and display table.

:has() is the parent selector: it styles an element based on what's INSIDE it. div:has(img) targets a div containing an image; label:has(+ input:invalid) targets a label whose adjacent input is invalid. CSS could not look upward for 25 years, and :has() ended that in every major browser by 2023.

Bottom line: :has() replaces the JS class-toggle pattern - the addHasImageToParent workflow that JavaScript frameworks institutionalized. A card containing a .badge gets a border; a form containing an invalid input gets a red header; a list item containing a sub-list expands - all in pure CSS. The wrapper-div-for-styling era is over.

The honest part: :has() invalidates upward. A change inside a matched child restyles the parent and all its children - fine for forms and cards, expensive on huge lists. And the browser support cutoff is real: this is a 2023+ feature, so legacy browser support still needs a JS fallback for critical layouts.

How to use

  1. Start with the parent pattern: div:has(img) - the simplest case that replaces a JS class toggle.
  2. Combine with other selectors: label:has(+ input:invalid) uses the adjacent sibling combinator AFTER :has() to scan forward.
  3. Test the invalidation cost: :has() on huge lists (1000+ children) can cause performance issues - gate with a class or narrow the scope.

Frequently asked questions

What makes :has() different from every other CSS selector?

Every other selector works DOWNWARD (descendant, child) or SIDEWAYS (sibling) - :has() is the first that works UPWARD, letting a parent be styled based on its children. This breaks the fundamental CSS rule that styles flow down, which is why it took 25 years and required rethinking the engine's invalidation model. The parent selector was the single most-requested CSS feature of all time.

How does :has() work with the + combinator for form validation?

label:has(+ input:invalid) scans FORWARD from the label to the adjacent sibling (the input) and styles the label if that input matches :invalid. The + combinator inside :has() means the input must be the IMMEDIATE next sibling. This enables error-state styling on the label without JavaScript - the browser does the validation check and the selector does the styling in one line of CSS.

Is :has() a performance problem?

It can be, on the wrong scale. :has() creates an upward invalidation: when a child inside a :has()-matched subtree changes, the browser must re-evaluate the parent and potentially all its descendants. For forms and cards (tens of children), this is negligible. For a list with thousands of items, it can cause jank. The mitigation: scope :has() to a reasonable subtree, and avoid it on flat lists where a class toggle would work.

Does :has() work in every browser?

Every major browser shipped it in 2023: Chrome 105, Safari 15.4, Firefox 121. That makes it broadly available in 2024+, but legacy enterprise environments and older mobile browsers may lack it. The progressive enhancement pattern: use @supports selector(:has(*)) to gate styles behind support, and keep a JS fallback (MutationObserver adding a class) for critical layouts only.

Related tools