Rahul Chhodde I'm a software developer with over seven years of experience in different web technologies.

The advanced guide to the CSS :has() selector

6 min read 1850

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.

Contents

Why is the :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.

Enabling support for the :has() selector

If 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.

experimental web platform features tab chrome


More great articles from LogRocket:


What does the CSS :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) { ... }

Chainability

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) {
  ...
}

Argument list for multiple selections

You can also provide a list of multiple element selectors, similar to chaining, but much more efficient:

.selector:has(div, .class, #id) {
  ...
}

Flexibility

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.

Usage scenarios

Let’s take a look at some example scenarios in which CSS’ :has() selector comes in handy.

Selecting the parent

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

Checking for multiple children

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()

Lorem ipsum dolor sit amet consectetur adipisicing elit. Ab pariatur, obcaecati accusantium alias minus repellat itaque amet libero corrupti maiores consectetur quasi! Eligendi ratione eaque et tenetur assumenda neque reprehenderit. Unde, nemo cumque molestiae mollitia impedit voluptate quisquam laborum ut, vitae aliquid totam numquam possimus dignissimos beatae quos corporis alias quod magnam quidem.

 

Selecting the previous sibling

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

Conditional decorations

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.

Styling empty states

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

Type and block adjustments

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()

Lorem, ipsum dolor sit amet consectetur adipisicing elit. Fugit corrupti perferendis quod laborum explicabo ratione quidem laudantium officia odit vitae, quisquam ullam aliquid voluptate consequatur eos quis ducimus, repellat aperiam. Hic tenetur modi explicabo itaque incidunt, obcaecati fugiat consequuntur asperiores aliquid, eius, atque ullam unde. Aut et molestias eaque repudiandae!

Iconized CTAs

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

Layout adjustments

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.

Better form usability

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

Checking for browser support

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

Conclusion

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.

Is your frontend hogging your users' CPU?

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.https://logrocket.com/signup/

LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app or site. 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 — .

Rahul Chhodde I'm a software developer with over seven years of experience in different web technologies.

Leave a Reply