Under The Hood
frontend css CSS Selectors Level 4 (:has, Chrome 105+, Safari 15.4+, Firefox 121+)

:has() Relational Selector: Invalidation Cost & Engine Heuristics

Last updated
Prerequisites:
CSS selectors and the cascade
Understanding of DOM structure and style recalculation
  • css
  • has-selector
  • selectors
  • performance
  • invalidation
  • engine

Read at your depth

The practical view

:has() lets you style an element based on what it contains or what comes after it: article:has(img) { padding: 0; } styles articles that contain an image. .card:has(.badge) { border-color: red; }. It also enables parent selectors and 'previous sibling' effects: .input:has(+ .error) { border-color: red; }. This removes the need for JavaScript class toggling in many cases. Modern browsers support :has() everywhere. Keep selectors simple (direct child or descendant with a class/tag) — the engine optimizes simple cases well.

The same idea in other frameworks

react equivalent
React conditional classes can be replaced by :has() when styling depends on contained state, reducing JS class juggling.

Legacy vs modern

JavaScript class toggling vs pure CSS :has()

Manually syncing classes with JS is error-prone and causes layout thrash; :has() expresses container-driven styling declaratively in the stylesheet.

before → after
JS class toggling
const card = document.querySelector('.card');
if (card.querySelector('.badge')) card.classList.add('has-badge'); // ❌ OLD
Pure CSS :has()
.card:has(.badge) { border-color: red; } /* ✅ NEW: declarative, no JS sync */

Interview gotchas

Context

Interviewers ask this to separate CSS syntax knowledge from an understanding of the style-resolution and invalidation machinery behind the selector.

The mechanical answer

Normal selectors resolve from the subject upward (right-to-left) using the element's own properties. :has() reverses the direction: the engine must search the subject's subtree (or siblings for the + combinator) to test whether a matching relative exists. The cost scales with subtree size and selector complexity, so the Selectors spec limits the argument forms engines must optimize. Browsers implement fast paths for simple arguments (e.g. :has(.class), :has(#id), :has([attr])) using cached per-element match sets, and they walk only ancestors that could be affected when a subtree mutates, with early-exit heuristics. Very complex arguments (deep nesting, many combinators, :has inside :has) degrade to slower probing.

Trap

The naive answer is ':has() is slow, avoid it'. Modern engines handle common forms efficiently; the cost is real only for pathological arguments. Another trap: thinking :has() with :scope, :not, or complex combinators costs the same as :has(.single) — each added level multiplies the subtree probing. Candidates who explain fast paths and invalidation heuristics show real understanding.

Under The Hood — a multi-depth technical interview hub.

Third-party licenses Press ⌘ K to search.