Learn CSS :has() selector by examples: 5 top use cases

November 18, 2024

4 min

Let's explore ten practical examples to learn CSS :has selector.

The :has selector in CSS opens up a world of new possibilities. Now that it's landed in Firefox 121, it's supported in all modern browsers.

In this blog, we're diving into some examples to learn all about :has in a practical way. Join me in exploring the power of this CSS selector!

1. Parent selector

The primary use of the :has() selector is as a parent selector. It helps check for the existence of an element within its parent.

For instance, consider a scenario where you want to verify the existence of an icon within a button:

button:has(.icon) {
	display: flex;
	gap: 10px;
}

Or, if you wish to append a dropdown arrow to a navbar item containing a submenu, it can be achieved like this:

nav li:has(ul) > a::after {
  content: "+";
  margin-inline: 10px;
}

We're searching for a list item (<li>) that contains a list (<ul>). Once we find it, we'll pick the link (<a>) inside and add a plus sign to it using a pseudo element. Here is a working demo for this example:

The :has() selector operates just like any other in your selector chain. You can include anything before, after, or within it. For example, targeting the .card element when it contains a <img> followed immediately by a paragraph:

.card:has(img + p) {
  flex-direction: row;
}

This allows us to create an adaptive layout for our cards based on their children:

You can also provide a list of elements separated by commas inside :has(), checking their presence within the parent element:

article:has(video, iframe) {
  /* Matches an <article> that contains either a video or iframe. */
}

This ensures the article has at least one of those children—either an <iframe> or a <video>.

Additionally, you can combine :has() with :not() for more complex scenarios:

.card:not(:has(img)) {
  /* targets .card elements that don't contain any descendant <img> elements. */
}

.post:has(img:not([alt])) {
  /* targets <img> elements inside .post elements that don't have the alt attribute specified. */
}

div:not(:has(:not(img))) {
  /* targets <div> elements that have only <img> elements as their children and no other elements. */
}

As you can see, one can get very creative with this selector but remember, you can't nest :has() selectors, however, you can chain them:

.headings:has(.subtitle:has(h2)) {
  /* Invalid */
}

.headings:has(h2):has(.subtitle) h2 {
  /* valid */
}

And finally, it's possible to check the state of child elements, to do things like form validation:

form:has(input:invalid) {
  border: 1px solid red;
}

You can check out the following demo for this example:

2. Previous sibling selector

The :has() selector isn't limited to targeting parents; it can also select previous siblings.

For instance, you can style a label based on the state of its immediate sibling, such as a checkbox:

<label for="checkbox">We need to target this when input is checked</label>
<input id="checkbox" type="checkbox">

And in your CSS:

label:has(+ input:checked) {
  color: green;
}

This CSS selector targets a <label> element that immediately follows a checked <input> element.

I added a working demo on CodePen for this example too:

We are not limited to selecting just the pervious sibling, we can target all the previous siblings as well. Consider a scenario with breadcrumb separators. In such an example, we don't want any separator after the last item in the list, so we can do this using :has() and a saubsequent-sibling combinator (~) to do the magic:

.breadcrumb-item:has(~ .current)::after {
  content: "/";
}

By the way, I learned this example from Eric Meyer.

This targets .breadcrumb-item elements having a .current element as their next siblings, not necessarily the immediate one.

But, what if we didn't use the .current class and wanted to find elements based on HTML tags only? You can achieve this using :has() too:

li:has(~ li:not(a))::after {
	content: "/";
	margin-inline-start: 10px;
}

We're selecting list items that have a list item without an <a> tag as their next sibling.

For the final example in this category, let's break down how to achieve the following effect using the same technique we've employed, using :has() as a selector for previous sibling.

I first saw this technique on Chris Coyier's CodePen.

The idea is to select two elements after and two elements before the one that's being hovered and add the transform effect to them.

li:has(+ li + li:hover) {
  transform: scaleY(1.1);
}

li:has(+ li:hover) {
  transform: scaleY(1.2);
}

li:hover {
  transform: scaleY(1.3);
  opacity: 1;
}

li:hover + li {
  transform: scaleY(1.2);
}

li:hover + li + li {
  transform: scaleY(1.1);
}

If you want see another cool and practical example for this use case, check out Stephanie's star rating component on CodePen.

3. Quantity queries

The :has() selector introduces the capability to perform quantity queries in CSS, allowing you to style a parent element based on the number of its children.

Here are some examples I swiped from Bramus' awesome blog post on the subject.

/* At most 3 (3 or less, excluding 0) children */
ul:has(> :nth-child(-n+3):last-child) {
	outline: 1px solid red;
}

/* At most 3 (3 or less, including 0) children */
ul:not(:has(> :nth-child(3))) {
	outline: 1px solid red;
}

/* Exactly 5 children */
ul:has(> :nth-child(5):last-child) {
	outline: 1px solid blue;
}

/* At least 10 (10 or more) children */
ul:has(> :nth-child(10)) {
	outline: 1px solid green;
}

/* Between 7 and 9 children (boundaries inclusive) */
ul:has(> :nth-child(7)):has(> :nth-child(-n+9):last-child) {
	outline: 1px solid yellow;
}

I saw a practical example for this on Twitter. Suppose you've set a max-height for a table but want to remove that limitation when the table has more data, specifically when it reaches 5 rows:

.table-wrapper:has(tr:nth-child(5)) {
  max-height: none;
}

Here is the demo if you want to play around:

This example shows how you can change styles dynamically depending on how much content is inside an element.

4. Anywhere selector

The :has() selector, when used with top-level elements such as html, body, or component roots, opens up a wide range of possibilities by allowing you to apply styles based on specific conditions within those elements.

For instance, you can switch themes based on a <select> element that exists somewhere in the document:

body:has(option[value="dark"]:checked) {
  --primary-color: #e43;
  --surface-color: #1b1b1b;
  --text-color: #eee;
}

In simple terms, we check if our body element has an option element checked with the value "dark." Then, we update our color variables. This can also be done for light and high contrast modes. You can see it in action in the following demo:

You can use the same technique to alter a layout as well. For instance, suppose you're looking for an radio input with a checked "list" value within the body and applying a specific style to a card-list somewhere in the DOM:

body:has(input[value="list"]:checked) .card-list {
  grid-template-columns: 1fr;
}

Feel free to give it a try yourself:

Also, I recently came across another practical example in Rob Bowen's article. It involved locking the scroll when a modal is open. By checking if the body contains a modal as a child, you can change the body's overflow property.

body:has(.modal) {
  overflow: hidden;
}

5. All-but-me selector

The all-but-me selector, made possible by :has(), helps choose all elements except the one being interacted with. For instance, when hovering over a child, you can select all its siblings except the one being hovered on. Check out this demo to see what we're dealing with here:

Here's the selector:

.card-list:has(.card:hover) .card:not(:hover) {
  filter: blur(4px)
}

This selector reads as follows: when the container has a child card hovered, select all the cards that aren’t hovered.

And that wraps up our last use case and example here. I've added all these examples to a collection on my CodePen account. I plan to include more examples in the future and keep the collection updated.

The video

Check out the YouTube video if you want to see all of this in action:

Conclusion

It's important to be careful not to overly depend on selectors like :has. Although powerful, excessive use might cause you to overlook simpler selector options. Striking a balance is crucial for crafting easy-to-maintain code!

If you're eager to explore more about the :has selector, my friend Geoff and I have written a blog post on CSS-Tricks. Feel free to check it out for more information!

Authors

Mojtaba SeyediMojtaba Seyedi

Share

xata
headless-wordpress
nuxtjs
storyblok
turborepo
render
gatsby
prismic
dato
github-pages
deno-deploy
nextjs
contentful
supabase
vercel
netlify
sveltekit
astro
bynder
strapi
hygraph
planetscale
sanity

Subscribe to newsletter.