CSS:has() — the parent selector

:has() — the Parent Selector

For years, "give me a CSS selector that styles a parent based on its children" was the single most requested feature CSS didn't have. The :has() pseudo-class finally delivers it. It matches an element if, and only if, at least one element matching the relative selector passed to it exists somewhere inside that element (by default, among its descendants).

Basic syntax

CSS
/* Match a .card ONLY if it contains an <img> */
.card:has(img) {
  display: grid;
  grid-template-columns: 120px 1fr;
}

/* Match a heading only if it is immediately followed by a paragraph */
h2:has(+ p) {
  margin-bottom: 0.5em;
}

The selector inside :has() is evaluated relative to the element :has() is attached to. img inside .card:has(img) means "an img anywhere inside this .card," while + p means "a p that is the next sibling of this element."

Worked example: form validation styling

A very common real-world use case is highlighting an entire form group when one of its inputs is invalid — something that previously required JavaScript to add a class to the parent element.

CSS
.form-group {
  padding: 0.75rem;
  border: 1px solid #ddd;
  border-radius: 6px;
  transition: border-color 0.15s ease;
}

/* Style the whole group differently if it contains an
   invalid, user-interacted input */
.form-group:has(:invalid) {
  border-color: crimson;
  background: #fff5f5;
}

.form-group:has(:invalid) label {
  color: crimson;
}

HTML
<div class="form-group">
  <label for="email">Email</label>
  <input id="email" type="email" required value="not-an-email" />
</div>
Worked example: cards that adapt to their content

:has() also lets a component adapt its own layout depending on whether an optional element is present, with no extra markup or modifier classes needed.

CSS
/* Default: a text-only card */
.card {
  padding: 1.5rem;
}

/* If the card happens to contain an image, switch to a
   media layout automatically */
.card:has(img) {
  display: grid;
  grid-template-columns: 96px 1fr;
  gap: 1rem;
  padding: 1rem;
}

/* Cards containing a .badge get extra top padding so the
   badge doesn't overlap the title */
.card:has(.badge) {
  padding-top: 2.5rem;
  position: relative;
}
More things :has() can do
  • body:has(dialog[open]) — style the whole page while a modal dialog is open, e.g. to disable scrolling.

  • a:has(> img) — target links whose direct child is an image (image-only links).

  • .field:has(input:focus) — highlight a field wrapper while its input is focused (an alternative to :focus-within when you also need to check for other conditions).

  • h1:has(+ h2) — select a heading only when a specific sibling follows it.

Note
Before :has(), "style a parent based on its children" required JavaScript: listening for events, toggling a class on the parent, and keeping that class in sync. :has() moves that logic entirely into CSS, and it stays reactive automatically — no event listeners to maintain. This is one of the most significant selector additions CSS has seen in years.