Home / Articles / Modern CSS /

Widely supportedno-js

:has() and no-JS interactivity

The parent selector: how to replace JS class toggles with a single CSS rule.

Why :has() matters

CSS has always been able to style elements based on their own state or their descendants (.parent .child). What it couldn't do was style a parent based on what it contains. That's why we wrote JavaScript like: input.addEventListener('focus', () => wrapper.classList.add('focused')). :has() makes that unnecessary.

:has() selects an element if it contains at least one element matching the argument. It's the CSS parent selector, and it shipped everywhere by 2024.

The patterns you'll use most

1. Form field highlight

/* Highlight the wrapper when a child input is focused */
.field:has(input:focus-within) {
  border-color: oklch(0.52 0.22 265);
  box-shadow: 0 0 0 3px oklch(0.52 0.22 265 / 0.15);
}

/* Show an error hint when input is invalid and touched */
.field:has(input:invalid:not(:placeholder-shown)) .error {
  display: block;
}

2. Nav dropdown

/* Style the nav item when its details is open */
.nav-item:has([open]) summary {
  color: oklch(0.52 0.22 265);
  background: oklch(0.92 0.06 265);
}
.nav-item:has([open]) .menu {
  opacity: 1;
  pointer-events: auto;
}

3. Card state

/* Lift a card when any focusable child has focus */
.card:has(:focus-visible) {
  outline: 2px solid oklch(0.52 0.22 265);
  outline-offset: 2px;
}

/* Change grid layout when a card is expanded */
.grid:has(.card[aria-expanded="true"]) {
  grid-template-columns: 2fr 1fr 1fr;
}

4. Sibling context

/* :has() can match relative to the document, not just a parent */
/* Hide the sidebar when a modal is open */
body:has(dialog[open]) .sidebar {
  visibility: hidden;
}

/* Change the page layout when the main nav is open */
body:has(.mobile-nav[open]) main {
  filter: blur(4px);
  pointer-events: none;
}

:has() for layout decisions

Beyond interactivity, :has() can change layout based on content structure — something previously impossible in CSS.

/* Full-width image when card has no text */
.card:not(:has(p)) .thumb {
  aspect-ratio: 4 / 3;
  border-radius: 0.75rem;
}

/* Two-column when there are exactly 2 list items */
ul:has(li:nth-child(2):last-child) {
  display: grid; grid-template-columns: 1fr 1fr;
}

Performance considerations

Early concerns about :has() performance (it requires checking subtrees) have been largely addressed by browser optimizations. In practice, the rules that are slow are the ones with unbounded scope: :has(*) or :has(.any-class) on a body-level selector. Use the most specific selector you can. Scope :has() to component-level selectors, not document-level ones, when possible.

Rule of thumb: .card:has(input:focus) is fine. body:has(*) is not. Specificity and scope matter.