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.

css /* Style a parent when it contains a focused element */ .card:has(button:focus-visible) { outline: 2px solid blue; }

That single capability has turned :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

Because :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:

css @supports selector(:has(*)) { /* :has() styles here */ }

Browsers that don’t recognize :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

One of the most practical uses for :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.

With HTML like this:

Title

You can style the container only when a keyboard user focuses the button inside it:

css @supports selector(:has(*)) { .bento-card:has(button:focus-visible) { outline: 2px solid currentColor; outline-offset: 4px; } .bento-card button:focus-visible { outline: none; } }

Note the double-check here: you need to remove the button’s own outline to prevent two focus rings. Also, :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()

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:

css .xy-pad:has(button:active), .xy-pad:has(button:focus-visible) { --dot-color: rebeccapurple; }

The variable --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

The most surprising use case for :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.

Previously, a React component might manage this with useEffect and imperative style manipulation, or by toggling a class on document.body. With :has(), the CSS does all the work:

css html:has([data-modal-open="true"]) { overflow: hidden; }

When the modal component conditionally sets 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

The same trick enables a pure-CSS theme switcher using a checkbox and the :checked pseudo-class:

css html:has(input#dark-mode-toggle:checked) { --bg: #1a1a1a; --text: #f0f0f0; }

Clicking the checkbox flips its :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.

WebKit’s Jen Simmons has documented this pattern in more depth, along with several other clever applications. The common thread is that :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

So far, every example has been about a parent reacting to a child. But :has() is not limited by the parent-child relationship. Consider a paragraph that immediately precedes a <figure>:

<style>  p:has(+ figure) {    font-weight: bold;  }</style><p>  This is a regular paragraph, with no  custom styles applied.</p><p>  This paragraph introduces this figure:</p><figure>  <img    src="/images/css-has/punk-cat.png"    alt="Photo of a hairless cat with a doodled mohawk"  ></figure><p>  See how the paragraph right before the <em>figure</em> was given bold text, while the other paragraphs like this one are untouched?</p>

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” (+), which matches an element that appears after another:

<style>  figure + p {    font-weight: bold;  }</style><p>  This paragraph comes <i>before</i> the figure.</p><figure>  <img    src="/images/css-has/fancy-cat.png"    alt="Photo of a gray cat with a doodled top hat and monocle"  ></figure><p>  This paragraph comes <i>after</i> the figure.</p>

Result

But + 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:

<style>  p:has(+ figure),  figure + p {    font-weight: bold;  }</style><p>  By combining the previous two selectors, we can select paragraphs on either side of the figure!</p><figure>  <img    src="/images/css-has/sparkly-cat.png"    alt="Photo of an orange cat with doodled glasses and sparkles"  ></figure><p>  See? This paragraph is bold too! ✨</p><p>  This paragraph is unselected, since it isn’t adjacent to a &lt;figure&gt;.</p>

Result

And you don't even need a sibling relationship. Because :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.

A dramatic example, adapted from Ahmad Shadeed's guide on :has(), demonstrates this. Try hovering over the category buttons as well as the books themselves:

<style>  html:has(    [data-category="sci-fi"]:hover  ) [data-category="sci-fi"],  html:has(    [data-category="fantasy"]:hover  ) [data-category="fantasy"],  html:has(    [data-category="romance"]:hover  ) [data-category="romance"] {    background: var(--highlight-color);  }</style><header>  Categories:  <ul>    <li>      <button data-category="sci-fi">        Sci-Fi      </button>    </li>    <li>      <button data-category="fantasy">        Fantasy      </button>    </li>    <li>      <button data-category="romance">        Romance      </button>    </li>  </ul></header><!--  These are 4 of my favourite books!  If you like sci-fi/fantasy, check  them out. 😄--><div class="book-grid">  <a data-category="sci-fi">    <img      alt="Book cover"      src="/images/css-has/psalm.jpg"    />    A Psalm For The Wild-Built  </a>  <a data-category="fantasy">    <img      alt="Book cover"      src="/images/css-has/season.jpg"    />    The Fifth Season  </a>  <a data-category="sci-fi">    <img      alt="Book cover"      src="/images/css-has/glory.jpg"    />    Some Desperate Glory  </a>  <a data-category="romance">    <img      alt="Book cover"      src="/images/css-has/winter.jpg"    />    Winter’s Orbit  </a></div>

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:

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);
}

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 category data attribute set to "sci-fi", and

  • Is currently in a hovered state.

Instead of styling the <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.

In many ways, :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

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 :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);
}

(The :where pseudo-class groups related selectors, so each clause does not need to be written separately.)

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 :has(), but for a real product I'd implement it in JavaScript.

In practice, I tend to use :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.

If you would like to dig deeper into :has(), these resources are well worth a look: