Reference guide · html-css-js · Published 2026-08-16 · 3 min read
The CSS :has() selector guide
Use the CSS :has() selector to style a parent from its children and cut JavaScript class toggling, without hurting performance.
- ·What it does
- ·Common patterns
- ·Performance notes
The CSS :has() selector, often called the parent selector, lets you style an element based on whether it contains a certain descendant. It inverts the normal direction of CSS: instead of selecting a child from its ancestor, you select an ancestor because of what is inside it. Since it reached baseline widely available support, it has become a practical way to replace the class-toggling JavaScript that used to be needed for content-based styling.
What it does
Written from the element you want to style, :has() takes a selector list and matches if the element has any matching descendant.
.card:has(img) {
border: 1px solid #ddd;
}
form:has(input:invalid) {
outline-color: #d33;
}
The first rule styles only cards that contain an image. The second styles a form while one of its inputs is invalid, which is the kind of state styling that previously required a script to add and remove a class.
You can also scope the argument to a direct child or a specific position. .card:has(> .media) and .list:has(:nth-child(3)) narrow the match in a way that reads clearly and keeps the selector cheap.
Common patterns
The highest-value uses are where a parent's appearance should depend on its content, a state that JavaScript controllers used to manage:
- Form validation. Style the fieldset or wrapper with
:has(input:required)or:has(:invalid)and drop the script that flipped a class. - Navigation highlight. Emphasise a list item that contains an active link:
li:has(a[aria-current="page"]). - Card variants. Select a card with a button, a badge or a given heading level without adding a variant class.
- Date pickers and tabs. Drive open or selected states from checked or targeted descendants.
Because it is a selector like any other, it composes with the rest of the cascade: you can use it alongside specificity, cascade layers and custom properties. It does not create new elements or new pseudo-elements, so a :has() version of a pattern is usually no heavier than the class-based version it replaces.
Performance notes
:has() has one performance caveat worth respecting. A complex parent selector re-evaluates when the DOM inside the document mutates, so a document-wide rule with a broad argument costs more than a narrowly scoped one. The guidance is to scope :has() to a component, such as .card:has(...) or #app :has(...), rather than to the document root, and to keep the argument as specific as you reasonably can.
In practice the vast majority of stylesheets will not hit a problem. The selector is baseline widely available, which means all current engines have shipped and stabilised it, and the performance concern is about pathological document-wide rules, not normal use. If you are styling hundreds of cards, a scoped component selector stays fast.
When :has() is doing work that used to live in JavaScript, you often also simplify the accompanying script. See css specificity to understand how its specificity competes, cascade layers for ordering, and javascript event delegation for the interactions that still genuinely need a script.