The CSS :has()
pseudo-class has been one of the most-awaited features for years. It’s a level 4 CSS selector, now available as a fully supported feature in Chrome 105 onwards, and will likely become a regular feature soon on other browsers as well.
The :has()
in CSS is a relational pseudo-class 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?:has()
selector:has()
selector do?:has()
selector necessary?Most developers rely on JavaScript for certain features 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 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 to check whether an element contains certain other elements or not, 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. An implementation of the above can be found here. Or, you can see the CSS :has()
alternative here.
Second is the capability to select the parent element from the children. Again, in lack of a CSS facility to do so, developers have relied on Web API’s parentNode property:
let el = document.querySelector(".someElement"), elParent = null if(el) { elParent = el.parentNode }
View a CodePen demo of the code above here, and how the :has()
selector implements this here.
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 (view a CodePen demo here):
let el = document.querySelector(".someElement"), elPs = null if(el) { elPs = el.previousElementSibling }
You can see how to perform this action with the :has()
selector here.
Everything we did above with JavaScript can now be achieved with CSS :has()
. We will cover them one by one in the next section after learning how to enable and test this feature on Google Chrome.
:has()
selectorIf you are on an older Chrome version (v.101-104), you can enable this feature from Chrome flags. Make sure 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 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 below code 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.
Selecting the parent from child with CSS has()
No Description
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.
Checking for multiple element with CSS has()
No Description
The selection of 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!
CSS :has() – Selecting Previous Sibling
No Description
Styling things with and without certain child elements separately can be avoided with the :has()
selector. Figure element with and without a caption is the perfect example here.
Conditional figure decoration with CSS :has()
No Description
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 it in action here.
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.
Styling Empty States with CSS :has()
No Description
While styling for an article, keeping type and block elements aligned is a tricky job. 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; }
The snippet above will produce uneven vertical spacing between headings and block elements, as shown here. But it is possible to make up for these irregularities using the previous sibling selection hack.
Type and Block adjustments using CSS :has()
No Description
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 is with an icon.
Now, if you are planning to write two separate classes for that, it can be easily avoided with the :has()
selector. The catch is to simply check the .btn
element for a .btn-icon
child and then style accordingly.
Simple Iconized CTAs with CSS :has()
No Description
Let’s say you have two layout variations for a header component: one is the fixed-width, and the other one is liquid, of course.
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.
Wrapper adjustments with :has()
No Description
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.
Adjusting Grid according to the number of items with CSS :has()
No Description
As you can see above, the grid automatically adjusts to three columns as soon as it exceeds the mark of two items.
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.
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.
Interactive HTML Form with CSS :has()
No Description
The above-discussed examples show an error dialog if no support to the :has()
selector is found. This can be done using the @supports
CSS rule, as shown in the below code snippets:
@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 { ... } }
The same can also be used to prompt the user if no support was found.
You can also use the Support API in JavaScript to detect the 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() }
In the article above, we 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.
We also discussed the current support offered to it by different browsers, and learned how to check for the browser support.
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.
Mutative processes data with better performance than both Immer and native reducers. Let’s compare these data handling options in React.
Radix UI is quickly rising in popularity and has become an excellent go-to solution for building modern design systems and websites.
In this article, we’ll explore CSS cascade layers — and, specifically, the revert-layer
keyword — to help you refine your styling strategy.
Nushell is a modern, performant, extensible shell built with Rust. Explore its pros, cons, and how to install and get started with it.
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?