The Selector That Works Bottom-Up
For years, CSS selectors flowed one way: from parent to child. You could style a `` because it lived inside a ` `, but you could never style that ` ` based on the `` it contained. The `:has()` pseudo-class flips that model, letting authors target a parent element based on what’s inside it. That single capability has turned Because Browsers that don’t recognize One of the most practical uses for With HTML like this: You can style the container only when a keyboard user focuses the button inside it: Note the double-check here: you need to remove the button’s own outline to prevent two focus rings. Also, The same principle extends to other pseudo-classes. In a custom drag-to-adjust control, you can tint the entire component while the handle is being pressed: The variable The most surprising use case for Previously, a React component might manage this with When the modal component conditionally sets The same trick enables a pure-CSS theme switcher using a checkbox and the Clicking the checkbox flips its WebKit’s Jen Simmons has documented this pattern in more depth, along with several other clever applications. The common thread is that So far, every example has been about a parent reacting to a child. But Result Here, the paragraphs and figures are siblings — there is no parent-child relationship in play. This might sound similar to the long-standing “next-sibling combinator” ( Result But Result And you don't even need a sibling relationship. Because A dramatic example, adapted from Ahmad Shadeed's guide on Result Hovering a category button highlights that button and every book in the matching category. Likewise, hovering over any book highlights its associated category button. The CSS is dense for a playground, so here is its core logic in a clearer layout: The first part of the selector reuses the same “global detection” pattern seen earlier. It checks whether the document contains any node that: Has its Is currently in a hovered state. Instead of styling the In many ways, This level of power is impressive, and it eliminates a host of JavaScript workarounds. But just because something can be done with CSS doesn't make it the right tool. My usual preference is for whichever technique adds the least complexity — and CSS often wins there, as it tends to be simpler than its JavaScript equivalent. With (The If this interface lived in something like React, I suspect a state variable tracking the active category would be simpler. It would also be more adaptable: categories could be dynamic, books could span multiple categories, and the feature would function in older browsers. The pure-CSS solution is a fantastic demonstration of In practice, I tend to use If you would like to dig deeper into Selecting Previous Siblings With CSS :has(), by Tobias Ahlin CSS :has() Interactive Guide, by Ahmad Shadeed Level Up Your CSS Skills With The :has() Selector, by Stephanie Eckles:has() into one of the most versatile additions to modern CSS. It’s supported in all major browsers—Safari 15.4, Chrome/Edge 105, and Firefox 121—and as of late 2024, it works for roughly 92% of users. The remaining 8% can still get a functional experience if you treat :has() as an enhancement rather than a dependency.Feature Detection With
@supports:has() can’t be polyfilled with older selectors, the safest use case is progressive enhancement. The @supports rule lets you scope your :has() styles behind a feature check::has() will skip the block entirely. Older browsers that don’t even support @supports will also ignore it. Either way, your fallback styles remain intact for users on legacy software.Moving Focus Outlines to the Parent
:has() is fixing awkward focus indicators in composite widgets. On a card layout where child elements change size on interaction, a standard focus ring on the child can look broken or clip unexpectedly. The fix is to shift the focus indicator to the parent.Title
:focus-visible only triggers for keyboard navigation, not mouse clicks, so pointer users won’t see any outline at all—which is the intended accessibility pattern.Using Other Pseudo-Classes Inside
:has()--dot-color can then drive the stroke and fill of dynamic SVG elements inside the control, giving clear visual feedback during both mouse drags and keyboard adjustments.Global Event Detection Without JavaScript
:has() is acting as a document-wide condition. You can target the `` element itself when any matching state exists anywhere in the DOM. That makes it possible to disable page scroll when a modal is open, without writing a single line of JavaScript.useEffect and imperative style manipulation, or by toggling a class on document.body. With :has(), the CSS does all the work:data-modal-open="true" in its render output, the selector matches and scrolling locks. Removing the attribute—or the element entirely—restores scrolling automatically.A JavaScript-Free Dark Mode Toggle
:checked pseudo-class::checked state, which immediately satisfies the :has() condition and overrides the CSS variables that define the theme. It’s not a full dark-mode implementation—you still need JavaScript to persist preferences or detect the OS setting—but it does demonstrate how much state logic you can offload to the selector engine.:has() turns CSS from a purely presentational language into one that can react to structural and state-based conditions across the entire document.Selecting backwards and across the DOM
:has() is not limited by the parent-child relationship. Consider a paragraph that immediately precedes a <figure>:+), which matches an element that appears after another:+ only works in one direction: forward. By pairing it with :has(), you can mirror that logic and select the element that comes before a given sibling. Together they cover both directions::has() accepts any relative selector, it can inspect the entire document. This opens up styling one element based on something that lives in a completely different container.:has(), demonstrates this. Try hovering over the category buttons as well as the books themselves:html:has([data-category="sci-fi"]:hover) [data-category="sci-fi"] {
background: var(--highlight-color);
}html:has(
[data-category="sci-fi"]:hover
) [data-category="sci-fi"] {
background: var(--highlight-color);
}
category data attribute set to "sci-fi", and<html> tag itself, the rule targets every descendant whose category data attribute is also "sci-fi". In plain terms: “If at least one hovered element in the document carries category="sci-fi", apply a lilac background to every element that does.” The buttons and the books don't share a parent or a sibling — the only commonality is that both are descendants of the root <html> tag.:has() feels like the missing selector CSS never had. A wide range of relationships were previously impossible to express. With :has() you can select any element based on the properties or state of any other element in the document.Power versus complexity
:has(), however, complexity can creep in quickly. Consider a production-ready version of that book UI, with controls that also work on mobile and for keyboard users:html:where(
:has([data-category="sci-fi"]:hover),
:has([data-category="sci-fi"]:focus-visible),
:has([data-category="sci-fi"]:active),
) [data-category="sci-fi"],
html:where(
:has([data-category="fantasy"]:hover),
:has([data-category="fantasy"]:focus-visible),
:has([data-category="fantasy"]:active),
) [data-category="fantasy"],
html:where(
:has([data-category="romance"]:hover),
:has([data-category="romance"]:focus-visible),
:has([data-category="romance"]:active),
) [data-category="romance"] {
background: var(--highlight-color);
}:where pseudo-class groups related selectors, so each clause does not need to be written separately.):has(), but for a real product I'd implement it in JavaScript.:has() for far more modest ends. It is excellent for small enhancements like the focus outlines on my “About” page, or for stopping mobile scroll. In those contexts it fits neatly within a React application.:has(), these resources are well worth a look:



