CSS could always style a child based on its parent. What it could never do was the reverse: style a parent based on its children, or an element based on its siblings. That one missing direction forced a staggering amount of JavaScript into codebases whose only job was to add and remove a class on some ancestor. The :has() pseudo-class closes the gap, and it is genuinely available now rather than "coming soon."

The syntax is small. :has() takes a selector list and matches the element if any of those selectors match something inside its scope. The trick to reading it fluently is to say the word "containing" out loud: figure:has(figcaption) is "a figure containing a figcaption."

Rather than explain it abstractly, here are five patterns that each used to require a script.

A form that reacts to its own validity

You want the submit button styled differently while any field in the form is invalid. Historically: validate on input, toggle a class on the form. With :has(), the stylesheet watches for you.

form:has(:invalid) button[type="submit"] {
  opacity: 0.5;
  pointer-events: none;
}

The button responds live as fields pass and fail, because :has() is dynamic — it re-evaluates as the DOM and its states change.

Layout that adapts to content, not viewport

A media object should sit in two columns when it has an image and one column when it does not. Media queries can't see content. :has() can.

.entry:has(img) {
  display: grid;
  grid-template-columns: 200px 1fr;
}

Quantity queries

Combine :has() with :nth-child logic and you can style a container based on how many things it holds. A gallery that tightens its gap once it passes six items:

.gallery:has(> :nth-child(7)) {
  gap: 0.5rem;
}

The floating label, finally without a hack

The classic floating-label trick relied on the adjacent-sibling combinator and a carefully ordered DOM. With :has() you can style the label based on the input's state directly, whatever the order:

.field:has(input:focus) label {
  color: var(--accent);
  transform: translateY(-1.2em) scale(0.85);
}

Reacting to a specific descendant far below

Because :has() reaches to any depth, a page can respond to something buried deep inside it. Give the whole article a wider measure when it contains a full-bleed table:

article:has(table.wide) {
  max-width: none;
}

The performance worry, addressed

The reasonable question is whether a selector that inspects descendants is expensive. In practice, engines evaluate :has() carefully and it performs well for the patterns above. The advice that holds up: keep the inner selector reasonably specific and scoped rather than open-ended, and prefer child combinators (:has(> img)) over descendant ones when the structure allows it, so the browser has less to walk. You are also, in almost every case, replacing a JavaScript mutation-and-repaint cycle — so the honest comparison is not "free versus :has()" but ":has() versus a script doing more work less efficiently."

The shift in what CSS can own

The interesting consequence isn't any single pattern — it's the category of logic that moves out of JavaScript entirely. A great deal of front-end scripting was never really behaviour; it was styling that happened to depend on state CSS couldn't observe. Validity, presence, count, focus somewhere in a subtree: CSS can see all of it now. Before you write another event listener whose whole purpose is toggling a class on a parent, check whether a selector can simply describe the condition instead. More and more often, it can.