The CSS Parent Selector Finally Arrives
A deep technical guide to the CSS :has() pseudo-class, covering parent selection, direct child and sibling selection, quantity queries, combining it with :is() and :not(), the nesting restriction, performance guidance, and browser support.

Every CSS selector has always described a path downward or across the document, an element, its descendants, or its following siblings, never upward. Styling a card differently because it happens to contain an image, or a form field wrapper differently because the input inside it is invalid, has always meant reaching for JavaScript to add a class somewhere higher up the tree, because CSS itself had no way to look at an element’s children and then style that element based on what it found. The :has() pseudo-class closes that gap.
Why CSS Could Never Select a Parent#
A combinator like the descendant combinator or the child combinator always narrows a selection from an ancestor toward its descendants, never the other way around. There has never been a mechanism in the selector grammar for saying select this element if a descendant matching some condition exists inside it, because evaluating that condition requires looking forward through the tree from the element being matched, which none of the existing combinators do.
The workaround has always been JavaScript: attach a MutationObserver or run logic on relevant events, check whether the condition holds, and toggle a class on the ancestor by hand. It works, but it means a purely visual, structural condition ends up depending on script running correctly and staying in sync with the DOM.
The Relative Selector Argument#
:has() takes what the specification calls a relative selector list, one or more selectors that are evaluated as if anchored to the element :has() is attached to, rather than to the document root.
.card:has(img) {
grid-template-columns: 120px 1fr;
}
This selects any element with the class card that contains an img element anywhere inside it, and applies a layout that only makes sense once an image is actually present.
Selecting a Direct Child Only#
Passing a relative selector that starts with a combinator narrows the match to a specific relationship rather than any descendant at any depth.
.list:has(> .featured) {
border-left: 3px solid goldenrod;
}
> .featured restricts the match to a direct child, so a .featured element nested two levels deeper inside .list would not trigger this rule the way an unqualified .featured argument would.
Selecting a Previous Sibling#
The next-sibling and subsequent-sibling combinators inside :has() reach in the other direction entirely, letting an element’s style depend on what comes after it in the document.
h1:has(+ h2) {
margin-bottom: 0.25rem;
}
This tightens the spacing under an h1 only when it is immediately followed by an h2, something no prior CSS selector could express, since every existing sibling combinator only ever looked backward from a later element to an earlier one, never forward.
A Checkbox Driven Theme Toggle Without JavaScript#
Combining :has() with the long standing checkbox hack removes the last piece of JavaScript a purely visual light and dark theme toggle used to need.
body:has(#theme-toggle:checked) {
--background: #111111;
--foreground: #f5f5f5;
}
A single checkbox, styled to look like a switch, flips a set of custom properties on the entire document the moment it is checked, entirely through selector matching.
Using () as a Quantity Query#
:has() can also count, indirectly, by checking whether a specific position in a list is occupied.
.grid:has(> :nth-child(4)) {
grid-template-columns: repeat(4, 1fr);
}
This switches a grid to four columns only once it actually has a fourth child, a pattern generally called a quantity query, useful for a layout that should respond to how much content it holds rather than to a fixed breakpoint.
Combining () with () and ()#
:has(), :is(), and :not() all take a selector list and compose freely with each other, which keeps a condition readable instead of forcing it into one long, repetitive selector.
:is(article, section):has(> h2):not(.no-toc) {
scroll-margin-top: 2rem;
}
This applies to an article or section that has a direct h2 child, excluding anything carrying a no-toc class, all in one rule rather than three separate ones.
A Real Nesting Restriction#
:has() cannot be nested inside another :has(), and a pseudo-element cannot appear as part of the selector passed into it. Both restrictions exist to keep the browser’s evaluation of :has() bounded, since the relational lookahead it performs is already more expensive than a normal selector match, and nesting it inside itself would compound that cost without a corresponding limit on how deep the lookahead can go.
Performance Considerations#
:has() forces the browser to re-evaluate whether an element still matches every time something inside it changes, which makes how broadly it is anchored matter more than it would for an ordinary selector.
| Pattern | Why it matters |
|---|---|
body:has(.sidebar-open) | Anchoring to body or :root means the browser watches nearly the entire document for a matching change |
.layout:has(.sidebar-open) | Anchoring to the smallest element that could plausibly contain the condition narrows the watched subtree significantly |
.ancestor:has(> .foo) | A direct child combinator limits the lookahead to one level instead of the entire subtree |
.ancestor:has(.foo) | An unconstrained descendant selector inside :has() is the most expensive form, since it has to check the full subtree on every relevant mutation |
None of this means :has() is unusable at scale, but a selector anchored as close as possible to the actual condition, using a direct child combinator where the relationship allows it, keeps the browser’s work proportional to a small part of the page rather than the whole document.
Browser Support#
:has() is supported in Chrome, Edge, Firefox, and Safari, and has been for long enough that it counts as Baseline widely available rather than a recent or partial addition. It is one of the more safely adoptable CSS features covered in this series, with essentially no need for a fallback path in a project that already targets current browser versions.
Conclusion#
:has() gives CSS a capability every other selector in the language lacked, the ability to style an element based on what is inside it or what follows it, rather than only on what it descends from. Parent selection, direct child conditions, forward looking sibling rules, and quantity queries are all the same underlying mechanism applied to different relative selectors, and all of them replace a pattern that used to require a script watching the DOM and toggling a class by hand. With support now broad across every major browser, it is one of the more immediately useful additions to the selector grammar in years.