:has(), with examples
:has() – the parent selector, previous sibling selector, anywhere selector, mother of dragons, and all-round powerhouse – was crowned the most-used and most-loved CSS feature in the State of CSS 2025 report.
Put simply, :has() is a pseudo-class that selects elements only when they match the relative selector list passed as its argument. It’s been part of baseline support since 2023, meaning it now works seamlessly across all major browsers.
Before :has(), styling a parent or a preceding element based on a child’s state meant relying on JavaScript workarounds. With full browser support finally here, :has() lets CSS respond to DOM context directly, making state-driven styling cleaner, more declarative, and far less dependent on scripting.
It’s used like this:
/* Select all .cards */
.card {
...
}
/* Select all .cards that contain an img */
.card:has(img) {
...
}
/* Select all .cards followed by another .card */
.card:has(+ .card) {
...
}
/* Select all buttons within said .cards */
.card:has(+ .card) button {
...
}
In this article, we’ll explore the many ways to use :has() to write cleaner, more efficient CSS, and rely a little less on JavaScript along the way.
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
:has() as a parent selectorLet’s start by exploring a few ways we can use :has() as a parent selector:
There are plenty of ways to use :has() in this context, but one particularly handy trick comes into play with CSS Grid (and, to a lesser extent, Flexbox). It lets you control essential grid or flex properties, the ones that must be defined on the parent, based on the structure or state of the children.
In short, when working with CSS Grid, you usually need to define how many rows or columns a grid has. But if that layout depends on the number of child elements, you can combine :has() with child pseudo-classes to make it dynamic.
In the example below, the grid updates its grid-template-columns value depending on the :nth-child() position of its direct :last-child. Since the grid has two children, the :nth-child(2) rule sets grid-template-columns: repeat(2, 1fr):
<div id="grid">
<div>First child</div>
<div>Second/last child</div>
</div>
#grid {
display: grid;
/* One child, one column */
&:has(> :last-child:first-child) {
grid-template-columns: repeat(1, 1fr);
}
/* Two children, two columns */
&:has(> :last-child:nth-child(2)) {
grid-template-columns: repeat(2, 1fr);
}
/* Three children, three columns */
&:has(> :last-child:nth-child(3)) {
grid-template-columns: repeat(3, 1fr);
}
/* And so on... */
}
See the Pen
:has() demo (conditional grid columns) by Daniel Schwarz (@mrdanielschwarz)
on CodePen.
While this is a simplified example and one that might not strictly require CSS Grid, it’s still the cleanest solution to problems of this kind. This approach becomes especially useful when you need to define grid rows or columns conditionally, or when working with more complex, data-driven layouts.
Note: The :last-child selector isn’t strictly required, but it helps prevent rule overwrites – a common source of subtle layout bugs.
Here, “parents” refers to elements like <label>, <fieldset>, <legend>, or any other form control container that might need to reflect the state of the controls inside it.
In the example below, we target <label> and <fieldset> elements that contain invalid form controls (:invalid). We also style the <legend> within those <fieldset>s. You could just as easily match :user-invalid, :valid, :user-valid, :checked, or almost any other state – :has() makes all of these cases possible:
<form>
<fieldset>
<legend>Name</legend>
<label>First name <input required type="text"></label>
<label>Last name <input required type="text" value="Rocket"></label>
</fieldset>
<fieldset>
<legend>Choose your fighter</legend>
<label><input required type="radio" name="fighter" value="Kayuza">Kayuza</label>
<label><input required type="radio" name="fighter" value="Jin">Jin</label>
<label><input required type="radio" name="fighter" value="King">King</label>
<label><input required type="radio" name="fighter" value="LogRocket">LogRocket</label>
</fieldset>
<button type="submit">Submit</button>
</form>
/* Selects labels that contain invalid form controls */
label:has(:invalid) {
color: red;
}
/* Selects fieldsets that contain invalid form controls */
fieldset:has(:invalid) {
background: hsl(from red h s l / 10%);
/* Selects legends within fieldsets that contain invalid form controls */
legend {
color: red;
}
}
See the Pen
:has() demo (invalid form controls) by Daniel Schwarz (@mrdanielschwarz)
on CodePen.
We can also style <label>s using :has() and the next sibling combinator (+), but this approach is less robust as the two elements must remain next to each other. The adjacent sibling combinator (~) is forgiving in regards to proximity, but is more error-prone as it also matches next siblings that aren’t next to each other:
<label for="id">Label</label>
<input required type="text" id="id">
/* Selects labels where the next sibling is an invalid form control */
label:has(+ :invalid) {
...
}
Note: You don’t need to use form:has(:invalid) since validity-based pseudo-classes already apply directly to <form> elements. However, if you want to match other states – like whether the form contains a checked input – you’ll need :has() for that:
/* Not necessary */
form:has(:invalid) {
...
}
/* Because this works */
form:invalid {
...
}
/* Necessary */
form:has(:checked) {
...
}
/* Because this doesn't work */
form:checked {
...
}
By the way, if you were planning to use :has() to target <label> elements whose nested form control is focused, you actually don’t need to. It’s a perfectly valid and even clever use case, but there’s already a pseudo-class made for this: :focus-within.
Since clicking or focusing on a <label> also focuses its associated control, :focus-within achieves the same goal and creates a larger, more accessible interaction area. You can use it like this:
label:focus-within {
...
}
Sometimes the solution depends on the tech you’re using. In the example below, we have three cards/blog categories. If we assume that the tech (e.g., a content management system or static site generator) embeds the blog category names into data attributes on a nested element, we could still declare different styles for each card depending on said names using :has(). Let’s assume that a CMS/SSG has created the following markup/constraints, and we can’t change it.
This is how we’d style the cards regardless:
<section>
<div><span data-category="Dev">...</span></div>
<div><span data-category="Product Management">...</span></div>
<div><span data-category="UX Design">...</span></div>
</section>
div:has([data-category*="Dev" i]) {
background: hsl(270 100% 50%);
}
div:has([data-category*="Product" i]) {
background: hsl(280 100% 50%);
}
div:has([data-category*="UX" i]) {
background: hsl(290 100% 50%);
}
See the Pen
:has() demo (attribute selectors) by Daniel Schwarz (@mrdanielschwarz)
on CodePen.
In the example above, we’re setting the background of each <div> based on part of a nested element’s data-category attribute. Specifically, [data-category*="Dev" i] targets any data-category value that contains “Dev” – case-insensitively (that’s what the i flag does). This means you could change the category name from “Dev” to “Development,” or from “UX Design” to just “UX”, and the CSS would still work.
Different attribute selectors let you match different kinds of values, but if your markup is structured like this, with key data nested inside, :has() might be the only practical way to achieve this level of control purely with CSS.
:has() as a previous sibling selectorWhile CSS has a next sibling combinator (e.g., element + nextelement, which selects nextelement if it’s followed by element), it doesn’t have a previous sibling combinator. Luckily, we can use :has() to create an artificial previous sibling selector. As you can see from the example below, where the sibling that comes before the sibling being hovered is blurred out, it oddly works using the next sibling combinator:
<section>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
</section>
/* Blur if preceding the sibling being hovered */
div:has(+ :hover) {
filter: blur(0.5rem);
}
See the Pen
:has() demo (previous sibling selector) by Daniel Schwarz (@mrdanielschwarz)
on CodePen.
And of course, by using the adjacent sibling combinator (~), we can blur all siblings that precede the one being hovered:
/* Blur all siblings that precede the sibling being hovered */
div:has(~ :hover) {
filter: blur(0.5rem);
}
Cool effects aside, you can also use these techniques in many other ways. For example, if those cards were breadcrumb steps, you could make all steps prior to the current step green to show that they’ve been completed, and reduce the opacity of all steps after to show that they’re not selectable yet (remember, the next siblings don’t require the use of :has()):
<ul>
<li>Breadcrumb 1</li>
<li>Breadcrumb 2</li>
<li class="active">Breadcrumb 3</li>
<li>Breadcrumb 4</li>
<li>Breadcrumb 5</li>
</ul>
li:has(~ .active) {
color: green;
}
.active ~ li {
color: red;
}
See the Pen
:has() demo (style prev/next siblings) by Daniel Schwarz (@mrdanielschwarz)
on CodePen.
:has() as an anywhere selectorWe can use the concept above to demonstrate the anywhere selector as well, which is when we query a selector and then style a dependent if said selector matches. In the example below, when a card is hovered, the other cards are blurred out:
/* Blur all cards not being hovered */
section:has(div:hover) div:not(:hover) {
filter: blur(0.5rem);
}
See the Pen
:has() demo (anywhere selector) by Daniel Schwarz (@mrdanielschwarz)
on CodePen.
This is akin to (if not exactly the same as) the parent selector. The difference is that with the anywhere selector, the parent is more like an ancestor and the child is more like a descendant – they aren’t related in a meaningful way. In fact, the ancestor could be the :root and the dependent could be nested many, many levels deep.
In fact, we can code a light-dark mode toggle (or any toggle really) using this exact technique, but I’ll demonstrate light-mode since it’s a very popular feature that might be one day be a WCAG 3.0 (Web Content Accessibility Guideline 3.0) requirement.
Without :has(), light-dark mode requires JavaScript (as do all of the examples in this article). However, by using :has() as well as some other CSS features, we can create this functionality using just CSS.
To begin, you’ll want to declare the color-scheme property on the :root so that the user-chosen mode applies to the whole page. This is why we’re using :has() to begin with, to see whether a dark mode checkbox nested deeper (possibly much deeper) is checked.
Specifically, declare color-scheme: dark if the checkbox is :checked or color-scheme: light if it’s :indeterminate (or :not(:checked):
<label><input type="checkbox" id="dark-mode"> Dark mode</label>
:root:has(#dark-mode:checked) {
color-scheme: dark;
}
:root:has(#dark-mode:indeterminate) {
color-scheme: light;
}
Now for the styling – which, thanks to CSS’s relatively new light-dark() function, is surprisingly simple. In the example below, the background switches to white in light mode and to black in dark mode:
body {
background: light-dark(white, black);
}
See the Pen
:has() demo (state-check checkboxes) by Daniel Schwarz (@mrdanielschwarz)
on CodePen.
You’ll still need JavaScript to read user preferences though, and possibly pre-check that checkbox, but that’s to be expected:
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
document.querySelector("#dark-mode").checked = true;
}
So as you can see, :has() isn’t tied to any one specific purpose. What’s clear, though, is that without it, most of these examples would have required JavaScript. And that never quite feels right. CSS and JavaScript serve different roles, and relying on JS for state-based styling can introduce render-blocking or deferred execution, which isn’t ideal when defining a page’s default state.
Once you start using :has(), you’ll be amazed by how many problems it elegantly solves (“Wait – :has() can do that?”). After a while, your mindset shifts to, “Can I do this with :has() instead?”
Here’s one of the wilder examples I’ve used recently, involving the still-experimental Interest Invoker API:
/*
If keyboard-focused within a target of interest
(or target of partial interest, which are always keyboard-
triggered), the interest trigger that currently has interest
or partial interest has its hide-delay removed
*/
body:has(:target-of-interest :focus-visible, :target-of-partial-interest) [interestfor]:is(:has-interest, :has-partial-interest) {
interest-hide-delay: 0s;
}
Got any even crazier use cases? Share how you’ve been experimenting with :has() in the comments below – we’d love to see what you’ve built.
As always, thanks for reading!
As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, try LogRocket.
LogRocket lets you replay user sessions, eliminating guesswork around why bugs happen by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks.
LogRocket's Galileo AI watches sessions for you, instantly identifying and explaining user struggles with automated monitoring of your entire product experience.
Modernize how you debug web and mobile apps — start monitoring for free.

Kombai AI converts Figma designs into clean, responsive frontend code. It helps developers build production-ready UIs faster while keeping design accuracy and code quality intact.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the October 22nd issue.

John Reilly discusses how software development has been changed by the innovations of AI: both the positives and the negatives.

Learn how to effectively debug with Chrome DevTools MCP server, which provides AI agents access to Chrome DevTools directly inside your favorite code editor.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now