Editor’s note: This article was reviewed for accuracy by Chimezie Innocent on 20 June 2024 and updated to include new browser support information, discuss new patterns, best practices, and additional use cases for the CSS :has
selector, mention performance and accessibility considerations, and more.
The CSS :has()
pseudo-class is a level 4 CSS selector that is now available as a fully supported feature in many browsers. It’ a relational pseudo-class that allows you to check if a given element contains certain child elements, select it if any match is found, and then style it accordingly.
This article explains the need for the :has()
selector, its general usage, various applications and use cases from simple to advanced, browser compatibility, and the fallbacks.
:has()
selector necessary?Most developers rely on JavaScript for certain features that CSS does not support by default. However, web browsers today are more powerful than ever, which is opening doors to many new and interesting CSS features.
The :has()
selector is one such feature. It works on the parent rather than children and uses a comma-separated list of selectors as arguments, then looks for matches among the children of the element it’s representing. Its functionality is similar to that of the jQuery has()
method.
Let’s go over some of the general needs for the :has()
selector in frontend development that explain why this feature was in such high demand for years.
First is the possibility of checking whether an element contains certain other elements and then styling it accordingly. JavaScript was the only way to do this before, as you can see here:
let content = document.querySelector("#content"), headings = [] if(content) { headings = content.querySelectorAll("h1, h2, h3") } if(headings.length) { // do something }
In the code above, we have used querySelectorAll
, which is a Web API method to check a division element for headings. The method returns a NodeList
of the supplied elements.
An empty NodeList
would mean there are no elements present in the immediate parent. Here’s how the above implementation looks with JavaScript:
See the Pen Check if an element contains certain elements / CSS :has() emulation with JavaScript by Rahul C (@_rahul)
on CodePen.
You can see the CSS :has()
alternative below.
Second is the capability to select the parent element from the children. Again, without a way to do so in CSS, developers have relied on Web API’s parentNode
property:
let el = document.querySelector(".someElement"), elParent = null if(el) { elParent = el.parentNode }
Here’s how the code above might look in a JavaScript implementation:
See the Pen Selecting parent element with JavaScript by Rahul C (@_rahul)
on CodePen.
Like before, you can jump ahead to see how the :has()
selector implements this behavior.
Finally, there is the ability to select the previous sibling of a given element. The sibling selector in CSS allows you to select the next sibling, but there was no CSS-only way to select previous siblings. The JavaScript implementation looks like this:
let el = document.querySelector(".someElement"), elPs = null if(el) { elPs = el.previousElementSibling }
Here’s the CodePen demo for the above:
See the Pen Selecting Previous Sibling with JavaScript by Rahul C (@_rahul)
on CodePen.
You can see how to perform this action with the :has()
selector below.
Everything we did above with JavaScript can now be achieved with CSS :has()
. We will cover them individually in the next section after learning how to enable and test this feature on Google Chrome.
:has()
selectorNewer versions of major browsers have full support for this selector. However, if you are on an older Chrome version (v.101-104), you can enable this feature from Chrome Flags.
Ensure you have Chrome 101+ and navigate to chrome://flags from the browser’s address bar. Set the Experimental Web Platform features to Enabled, and you’re good to go. Relaunch the browser, and you can work with CSS :has()
in your Chrome browser:
:has()
selector do?Let’s explore the :has()
pseudo-class, its usage, and its properties. Because it’s a pseudo-class, it can be attached to any selector with a colon and accepts classes, IDs, and HTML tags as parameters.
The code below explains its general usage and syntax. The class .selector
only gets selected if it contains the elements passed to it as parameters using the has()
pseudo-class:
.selector:has(.class) { ... } .selector:has(#id) { ... } .selector:has(div) { ... }
You can chain multiple :has()
pseudo-classes one after the other whenever you see fit. The code below demonstrates how this chaining works:
.selector:has(div):has(.class):has(#id) { ... }
You can also provide a list of multiple element selectors, similar to chaining, but much more efficient:
.selector:has(div, .class, #id) { ... }
Suppose you accidentally provided an invalid element selector to the :has()
pseudo-class. It is intelligent enough to ignore that and consider only the valid selectors:
.selector:has(div, accordion, .class, ::lobster, #id) { ... }
accordion
and ::lobster
are invalid selectors and will be ignored in the above case. You’ll see no CSS errors or alerts in the developer tools.
Let’s take a look at some example scenarios in which CSS :has()
selector comes in handy.
This may be the most common use of the :has()
selector, because its default behavior is to select something if it contains a specific set of elements. But what if we are aware of only the child element?
The universal selector (*
) can be used here with :has()
and the child combinator (>
) to quickly select the parent without even knowing anything about it:
See the Pen Selecting the parent from child with CSS has() by Rahul C (@_rahul)
on CodePen.
As discussed in the properties segment above, :has()
allows you to pass a list of multiple entities, which means you can check for as many selectors as you want within a given element:
See the Pen Checking for multiple element with CSS has() by Rahul C (@_rahul)
on CodePen.
Selecting the previous sibling is made possible by combining the CSS adjacent sibling combinator with the :has()
pseudo-class.
As you may already know, the adjacent sibling combinator selects the very next sibling of a given element. We can use this behavior with has()
to get the previous sibling. Simply put, if an element has a next sibling, it’s easy to select it with :has()
and +
combinator:
See the Pen CodePen Home
CSS :has() – Selecting Previous Sibling by Rahul C (@_rahul)
on CodePen.
Styling things with and without certain child elements separately can be avoided with the :has()
selector. A good example is a figure element with and without a caption:
See the Pen Conditional figure decoration with CSS :has() by Rahul C (@_rahul)
on CodePen.
What if a figcaption
element is there but doesn’t contain any text? In that case, we can use the :not
and :empty
selectors to keep a check on the content.
Similar to figure decoration, we can easily switch the text alignment of block quotes with more than one paragraph:
See the Pen Conditional blockquote alignment with CSS :has() by Rahul C (@_rahul)
on CodePen.
We already have a pseudo-class called :empty
for styling elements that contain nothing. As far as targeting an empty state is concerned, it does not work as expected. One space inside is enough for it to recognize an element as non-empty.
It’s not ideal to use :empty
in this case. Let’s create a card grid that also contains a few contentless cards. We’ll be styling the empty card states with the :has()
selector:
See the Pen Styling Empty States with CSS :has() by Rahul C (@_rahul)
on CodePen.
While styling for an article, keeping type and block elements aligned is tricky. Consider an example of mixing code blocks, figures, and block quotes with the general type elements.
The block elements should have more vertical spacing and decorations to stand out among different typographical entities. Here’s some CSS to handle that:
p { margin: 0; } p:not(:last-child) { margin-bottom: 1.5em; } h1, h2, h3, h4, h5, h6 { line-height: 1.3; margin: 1.5em 0 1.5rem; color: #111; } pre, figure, blockquote { margin: 3em 0; } figcaption { margin-top: 1em; font-size: 0.75em; color: #888; text-align: center; }
As shown here, the snippet above will produce uneven vertical spacing between headings and block elements. However, it’s possible to make up for these irregularities using the previous sibling selection hack:
See the Pen Type and Block adjustments using CSS :has() by Rahul C (@_rahul)
on CodePen.
Suppose you are creating a CTA or button component in CSS with two variations: the first one is the default, plain button, and the second one has an icon.
Instead of writing two separate classes for these button variations, you can use the :has()
selector. The catch is to check the .btn
element for a .btn-icon
child and then style accordingly:
See the Pen CodePen Home
Simple Iconized CTAs with CSS :has() by Rahul C (@_rahul)
on CodePen.
Let’s say you have two layout variations for a header component: one is fixed-width, and the other one is liquid.
To keep the contents of the header within a fixed width, we have to add a content wrapper element inside it. The only difference between the markup of these two header versions is the wrapper element.
We can then pair the :has()
with :not()
selector and add the CSS classes, as shown in the below demonstration:
See the Pen Wrapper adjustments with :has() by Rahul C (@_rahul)
on CodePen.
Another example of adjusting the layout can be modifying the number of columns in a grid as soon as they reach a certain number.
This can be handy when you are not using the minmax()
function that determines the best width to fit a column in its grid:
See the Pen Adjusting Grid according to the number of items with CSS :has() by Rahul C (@_rahul)
on CodePen.
As you can see above, the grid automatically adjusts to three columns when it exceeds the mark of two items.
You can also use :has
selector to apply dark mode to your website. Let’s look at how we can achieve this:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>CSS :has selector</title> <link rel="stylesheet" href="style.css" /> </head> <body> <div> <select> <option value="">Select theme</option> <option value="light">Light mode</option> <option value="dark">Dark mode</option> </select> <section> <h1>Title</h1> <p> Lorem ipsum dolor sit amet consectetur adipisicing elit. Magnam dolor ipsum porro ducimus perferendis esse ut ad sint laboriosam sapiente. </p> <p> Lorem ipsum dolor sit amet consectetur adipisicing elit. Magnam dolor ipsum porro ducimus perferendis esse ut ad sint laboriosam sapiente. </p> </section> </div> </body> </html>
We have two select options: dark and light mode, which we will use to assign themes to our webpage.
Using :has
selector, we can implement this like so:
div { --heading-color: #0d6322; --background-color: #fff; --text-color: #216431; } div:has(option[value="light"]:checked) { --heading-color: #333; --background-color: #e4e4e44d; --text-color: #333; } div:has(option[value="dark"]:checked) { --heading-color: #b8973d; --background-color: #1e293b; --text-color: #b3b894; } div { padding: 2rem; font-family: sans-serif; } select { background: var(--background-color); border-radius: 5px; color: var(--text-color); font-size: 14px; transition: 0.1s all; margin-bottom: 20px; padding: 10px; } select:focus { border-color: var(--heading-color); } option { background-color: var(--background-color); color: inherit; } section { background-color: var(--background-color); border: 1px solid var(--text-color); border-radius: 8px; padding: 2rem; } h1 { color: var(--heading-color); } p { color: var(--text-color); }
With the CSS, we are dynamically changing the theme of our webpage based on the selected option in our dropdown menu. We used CSS variables to define our color schemes for heading, background, and text and we set our initial colors schemes for default i.e. when no theme has been selected.
div:has(option[value="light"]:checked)
checks for when the light
value has been checked and then changes the color schemes to a lighter theme. Similarly, div:has(option[value="dark"]:checked)
selector changes the CSS variables when the dark
value option is checked.
This showcases the flexibility and great use case for the CSS :has
selector.
:has
selectorThe :has
selector can help improve accessibility in so many ways. For example, we can conditionally style elements based on their content or the presence of assistive technologies.
Certain styles might be hidden from assistive technologies using the aria-hidden
attribute. In such cases, we can use :has
to target elements that visually contain hidden content and provide alternative styling or description.
Let’s look at how we can improve accessibility with :has
.
Using :has
selector in combination with ARIA attributes such as aria-current
and aria-hidden
can improve accessibility. Styling navigation classes and links or hidden content makes it easy to inform the assistive technology user about certain actions, states, or content on the page.
Let’s look at the example below:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Improving Accessibility with :has</title> <link rel="stylesheet" href="styles.css"> </head> <body> <nav class="menu"> <ul> <li><a href="/" aria-current="page">Home</a></li> <li><a href="#">About</a></li> <li><a href="#">Services</a></li> <li><a href="#">Contact</a></li> </ul> </nav> </body> </html>
In the code above, the aria-current="page"
attribute indicates the current page. Its purpose in accessibility is to inform the assistive technology user about the page and the link that has been clicked or set. With :has
selector, we can apply styles based on the aria-current
attribute:
ul li { list-style: none; display: inline-flex; margin-left: 10px; } .menu li a { color: green; text-decoration: none; } .menu li:has([aria-current="page"]) { font-weight: bold; border-bottom: 2px solid black; color: #f0f0f0; }
The style will apply to any link that has the aria-current="page"
attribute.
Interactivity is best defined when it’s properly connected to feedback. When it comes to interactive HTML forms, offering the user feedback about its input is the best thing you can do.
This is a good use case where :has
is used to improve form accessibility. It can be used to style form elements when they contain certain child elements like error messages, unfilled fields, or incorrect values without interfering with screen reader accessibility.
With the help of the :has()
, :valid
, and :invalid
pseudo-classes, we can make our forms a lot more dynamic and with no JavaScript involved:
See the Pen CodePen Home
Interactive HTML Form with CSS :has() by Rahul C (@_rahul)
on CodePen.
These are some ways where we can improve our accessibility with the CSS :has
selector. By creating contextual styles and providing better visual cues for navigations, including keyboard navigations, users can easily see which element is focused at any point.
Additionally, the :has
selector also helps to improve performance and reduce complexity since many accessibility features that previously required JavaScript can now be handled purely with CSS.
:has
selectorAlthough using the :has
selector is great for eliminating or reducing the amount of JavaScript needed, it may have performance implications if used excessively or with complex selectors.
It’s not necessarily optimal to use the :has
selector because, as many other CSS styles do, it requires the browser to evaluate or sort out the selectors and apply the style rule to each element in the DOM. Too many chained or complex selectors will be more resource-intensive compared to using fewer or simpler selectors.
When :has
is used with nested selectors, it can increase the load time on the browser. This is because the selector traverses the whole DOM, sorting out nodes or elements that meet the specified conditions. The more elements and conditions it needs to check, the longer it takes to apply the styles, which affects the overall performance.
Similarly, in scenarios where the DOM is updated dynamically, the browser has to re-evaluate and check the :has
conditions each render time and recalculates the layout of a portion or of the entire webpage. This can lead to performance bottlenecks like the Largest Contentful Paint (LCP) if not managed properly.
:has
selectorSince we now know that :has
can affect performance, what steps can we take to mitigate its impact? To reduce performance issues, you should consider the following practices when using the :has
selector:
:has
for scenarios where it greatly or significantly simplifies your CSS or enables functionality that would otherwise require JavaScript:has
selector globally, target specific elements or sections where it’s required or would enhance performance or functionality so the browser doesn’t have to evaluate the full weboage to apply your styles:has
selector too. Try to avoid chaining multiple complex conditions, as this can lead to performance issuesFinally, make sure you monitor performance. Always and regularly keep tabs on the impact of your styles on your website’s performance. Browser tools like Lighthouse can help you check for any performance issues and resolve them quickly.
:has
selectorBrowser support limited :has
selector usage in the past, especially with older browsers. However, major browsers like Chrome and Safari now offer full support of the selector in their latest versions. Below is list of browsers that offer full support for the :has
selector:
Since December 2023, the :has
selector feature works across the latest devices and browser versions. Nevertheless, to ensure compatibility and provide fallbackes, you can use the @supports
CSS rule to check if the :has
selector is supported by your browser:
@supports (selector(:has(*))) { .selector:has(...) { ... } }
Above is the progressive way to check for support and style the element as required. If a codebase uses some new features directly, here’s a way to write the backward-compatible versions of the same:
@supports not (selector(:has(*))) { .selector { ... } }
You can also use the above to prompt the user if no support was found.
The Support API in JavaScript also provides a way to detect browser support for different features. Here’s an example of that checking for :has()
support purely in JavaScript:
if(!CSS.supports('selector(html:has(body))'))) { // if not supported // Use the conventional JavaScript ways to emulate :has() }
Finally, most modern frontend frameworks — such as React, Vue, Next, and more — are compatible with the :has()
selector. You can even use it with Tailwind CSS as custom CSS in variants, and it will work just fine:
<div class="[&:has(p)]:bg-red-500"> <p>Hello World</p> </div>
In this article, we’ve learned about :has()
, a level 4 CSS selector. We covered its needs, some cases where it can replace JavaScript in your frontend projects, and general to advanced use cases.
There are so many other scenarios where you can use the :has
selector in your application and hence, we can’t cover them all. However, this article aims to show you where and how you can use it, as well as to broaden your awareness of its flexibility and many possible use cases.
Beyond this, we discussed the current support for the :has
selector offered by different browsers and learned how to check for browser support. We also looked at some performance considerations and best practices to consider when using the selector. Finally, we saw how we can improve our accessibility with the selector.
Thanks for reading all the way through. I hope you learned something new through this. Feel free to share more examples of how :has()
can be useful in the comments.
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 is like a DVR for web and mobile apps, recording everything that happens in your web app, mobile app, or website. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.
Modernize how you debug web and mobile apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
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 nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
2 Replies to "The advanced guide to the CSS :has() selector"
Thank you for this wonderful article.
Wow… wait a sec…
Forgive the “delusions of grandeur”, but given a JAMstack-like architecture with a pre-configured pre-rendered but hidden HTML layout of some sort (schema?), would `:has()`, maybe in combination with `:not(:empty)`, allow for a pure CSS UI to detect, rearrange, and show itself, solely based on the changes of its (dynamic) content?