What Is :has() in CSS? The Parent Selector Explained
:has() is CSS's relational pseudo-class — it lets a selector match an element based on what's inside or after it, finally enabling a parent selector.
The :has() pseudo-class lets a CSS selector match an element based on what’s inside it, or what follows it, instead of only what it is. For years this was the single most requested CSS feature — a “parent selector” — because CSS could style descendants based on ancestors but never the reverse. :has() closes that gap, and it does more than parent-matching: it’s a general relational selector.
The problem it solves
Before :has(), styling a container differently depending on its contents meant reaching for JavaScript. Want a card to get a highlighted border only when it contains an image? Want a form group to turn red only when its input is :invalid? CSS alone couldn’t express “select this element if that condition holds somewhere inside it.” You’d add a class with JS, or restructure your markup so the state lived on a wrapper element you could target directly.
:has() expresses that relationship natively:
/* Style a card only if it contains an image */
.card:has(img) {
border: 2px solid var(--accent);
}
/* Style a form group when its input is invalid */
.form-group:has(input:invalid) {
outline: 2px solid crimson;
}
How it works
:has() takes a relative selector as its argument and matches the reference element — not the thing inside the parentheses. In .card:has(img), the selector matches .card, not img. That’s the key mental model: :has() is a filter applied to the element on its left, based on a condition evaluated relative to it.
The argument can use combinators to describe more than direct children:
/* Direct child image */
.card:has(> img) { }
/* Any descendant image, at any depth (default) */
.card:has(img) { }
/* A heading immediately followed by a paragraph */
h2:has(+ p) { }
/* Any sibling that comes after with a specific class */
.item:has(~ .item.active) { }
That last pattern — matching based on following siblings — is something no other CSS selector could do before :has(). It effectively gives you a “previous sibling” selector by inverting the direction: instead of selecting the sibling that follows, you select the one that precedes it, conditioned on what comes after.
Real-world patterns
A few things :has() makes possible without JavaScript:
- Empty-state styling.
.list:has(> li) { display: block; } .list:not(:has(> li)) { display: none; }— show or hide a container based on whether it has children, no JS observer needed. - Form validation UI. Style a
fieldsetor label based on the validity state of the input inside it, using:has(:invalid)or:has(:checked). - Quiz or checkbox-driven layouts. Combine
:has()with:checkedto build accordions, tabs, or “select this card” UI purely in CSS, extending the old checkbox-hack pattern into something far more expressive. - Conditional grid/flex layouts.
.gallery:has(img:nth-child(1):last-child)can detect “this gallery has exactly one image” and apply a different layout, no class toggling required.
Specificity and performance
:has() contributes specificity based on the most specific selector inside its argument, similar to :not() and :is(). If you’re relying on this to override other rules, check the cascade carefully — see the guide to CSS specificity for how these calculations stack.
Performance-wise, :has() is more expensive to evaluate than a simple descendant selector because the browser has to check subtrees rather than a single ancestor chain. In practice this is rarely noticeable outside pathological cases (huge DOM trees combined with broad, unscoped :has() queries), but it’s worth scoping the reference selector as narrowly as you can — .card:has(img) rather than *:has(img).
How it fits with other modern CSS
:has() pairs naturally with other relational and container-aware features shipping around the same time. Container queries let an element respond to the size of its containing box; :has() lets an element respond to the contents of a box. CSS nesting makes the resulting selectors easier to write and read, since you can nest :has() conditions inside a parent rule rather than repeating the full selector chain. And because :has() selectors can get long, logical properties are worth combining with them so the layout rules you’re conditionally applying stay writing-mode-agnostic.
:has() vs JavaScript for this
:has() | JavaScript (MutationObserver, etc.) | |
|---|---|---|
| Reacts to DOM changes | Automatically, as part of style recalculation | Requires manual observer setup |
| Performance | Native, browser-optimized matching | Extra JS execution and observer overhead |
| Complexity for simple cases | One selector | Observer, callback, class-toggling logic |
| Complex conditional logic | Limited to selector expressiveness | Arbitrary logic |
For anything expressible as “does this element contain/precede an element matching X,” :has() is simpler and faster than wiring up an observer. For logic that depends on values, computation, or external state, you still need JavaScript.
The takeaway
:has() turns CSS’s one-directional descendant matching into a real relational query: style a parent based on its children, or a sibling based on what follows it. It removes a whole category of JavaScript that existed only to toggle a class in response to DOM structure. Scope the reference selector as tightly as you can, mind how it affects specificity, and it slots naturally alongside container queries and nesting as one more piece of CSS doing what used to require a script.
Tagged
Keep reading
Takina · · 4 min read CSS object-fit and object-position, Explained
object-fit controls how an image or video is cropped inside its box, and object-position controls which part of it stays visible. How they work together.
Takina · · 5 min read CSS inherit, initial, unset & revert Explained
CSS's four global keywords control where a property's value comes from. How inherit, initial, unset, and revert differ, with a comparison table.
Takina · · 4 min read Dynamic Viewport Units: dvh, svh, and lvh Explained
dvh, svh, and lvh fix the classic mobile vh bug where browser toolbars cut off full-height layouts. Here's what each unit measures and when to use it.