For years, CSS could describe a child based on its parent, but never the reverse: targeting a parent based on what it contains stayed impossible without JavaScript. The :has() pseudo-class changes that completely.
It is one of the most anticipated additions to the language, since it unlocks use cases that previously required a script to manually add a class based on an element's content.
Definition
The :has() pseudo-class targets an element if at least one of the selectors passed inside the parentheses matches something inside that element, or among what follows it depending on the combinator used in the argument.
The parent selector, finally possible
/* a card that contains an image changes style */
.card:has(img) {
border: 2px solid #2d6cdf;
}
/* a form that contains a field in error */
form:has(.field-error) {
background-color: #fff5f5;
}
/* a heading directly followed by a paragraph */
h2:has(+ p) {
margin-bottom: 8px;
}The first example is the clearest use case: a card that contains an image gets a border, while a card without an image gets none, with no class manually added anywhere to signal that difference.
Combining :has() with combinators
The argument passed to :has() can use any CSS combinators, which radically changes what is being checked. .card:has(img) checks for an image anywhere inside the card, while .card:has(> img) only checks direct children, without reaching further down the structure.
| Selector | Checks for |
|---|---|
.card:has(img) | An image anywhere inside |
.card:has(> img) | An image as a direct child only |
h2:has(+ p) | A paragraph right after the heading |
Use cases beyond the classic examples
:has() is also used to style a label based on the state of the checkbox it contains, to adapt a grid's layout based on how many items it holds, or to react to a :hover state on a child in order to change an ancestor's appearance, a relationship CSS simply could not express before.
:has() can carry a higher computational cost than a plain selector on very heavy pages, since the browser has to evaluate the content of every candidate element. On a normal site that difference stays negligible, but it is worth avoiding inside a loop that generates thousands of elements.Adapting a layout based on its content
:has() also makes it possible to adjust an entire layout based on what it contains, a use case that used to be impossible without a script. A grid that holds only one item can for instance receive a different layout than a grid holding several, simply by counting the possible matches inside :has()'s argument.
/* special layout for a grid with a single item */
.grid:has(> :only-child) {
display: flex;
justify-content: center;
}Combined with :is() and :where() to group several conditions, :has() opens the door to stylesheets that react intelligently to the actual amount of content present, a behavior that previously required counting elements server-side or in JavaScript before the page's very first render. This capability changes how reusable components get designed, since a single CSS block can now adapt to variable content with no condition written anywhere outside the stylesheet itself.
Frequently asked questions
:has() fully replace JavaScript for reacting to content?For pure styling, often yes. But :has() triggers no event and cannot run any logic, it stays limited to applying styles conditionally based on detected content.
:has() be nested inside a :has()?Yes, the specification allows it, but that level of nesting quickly makes the selector hard to understand and is better avoided in favor of simpler logic whenever possible.
:has() work across all recent browsers?Yes, modern browsers all support it by now, but it remains essential to check the exact compatibility target of a project before relying on it for an essential feature.