Watch our biggest AI launch event

Announcing Visual Copilot - Figma to production in half the time

Builder.io logo
Contact Sales
Contact Sales

Blog

Home

Resources

Blog

Forum

Github

Login

Signup

×

Visual CMS

Drag-and-drop visual editor and headless CMS for any tech stack

Theme Studio for Shopify

Build and optimize your Shopify-hosted storefront, no coding required

Resources

Blog

Get StartedLogin

‹ Back to blog

Web Development

Modern CSS for 2024: Use Cases for :has()

January 12, 2024

Written By Yoav Ganbar

All right, let's get real about the :has() pseudo-class in CSS.

It's kind of a big deal.

Think of :has() as the cool new tool in your CSS kit. It's all about checking if an element has a specific other element inside it. This isn't your everyday selector; it's a bit like having a secret power for your styles.

It's like conditional styling made easy.

But it's more than just looking at parent-child relationships. :has() is versatile. You can get creative with it and explore different ways to apply styles based on various element relationships.

In this post, we’ll explore a few different use cases on when and how you might want to use it.

You can find all the examples in this Codepen collection.

What it is and can you use it

:has() is classified as a level 4 CSS selector and has been implemented in Chrome 105 onwards (Firefox was the last hold out, but shipped it at the end of 2023 in version 121).

In a nutshell, it’s like a lookahead assertion in Regular Expressions.

Its introduction is significant as it allows for relational checks within the CSS, a feature that has been in demand for a considerable period.

Browser compatibility chart for the :has() selector

With global usage of 90% and it being available in all modern browsers, there’s no reason why you shouldn’t start using it today for your future projects!

I am a fan of Wes Bos and Syntax.fm in general.

Not long ago, he posted this on Twitter (X):

This was just when I was working on my modern CSS for 2024 post. I considered adding a section about the :has() selector, but felt it wouldn’t have done it justice.

Instead, I thought it would be cool to show all the use cases Wes showed in his post with some real-world examples.

Let’s dive into some has:() tips!

To showcase this section, we’ll be using the following design:

sample.png

I generated this design by using Builder.io Figma plugin, and then used Visual Copilot to generate the code.

You can find the initial code in this Codepen link.

This is our generated HTML:

<div class="main-container">
 <header class="header">
  <img loading="lazy" srcset="..." class="image" />
  <h1 class="title">Blog Post Title</h1>
  <div class="meta-data">Written by John Doe on October 10, 2022</div>
 </header>
 <p class="description">Lorem ipsum dolor sit amet...</p>
 <a href="#" class="read-more">Read More</a>
</div>

And CSS:

.main-container {
 background-color: #f1f1f1;
 display: flex;
 flex-direction: column;
 justify-content: center;
 padding: 8px;
}

.header {
 display: flex;
 flex-direction: column;
 padding: 40px 49px;
}

@media (max-width: 991px) {
 .header {
  max-width: 100%;
  padding: 0 20px;
 }
}

.image {
 aspect-ratio: 1;
 object-fit: contain;
 object-position: center;
 width: 800px;
 overflow: hidden;
 align-self: center;
 max-width: 100%;
}

.title {
 color: #000;
 text-align: center;
 align-self: center;
 margin-top: 65px;
 white-space: nowrap;
 font: 400 24px Arial, sans-serif;
}

@media (max-width: 991px) {
 .title {
  margin-top: 40px;
  white-space: initial;
 }
}

.meta-data {
 color: #666;
 text-align: center;
 align-self: center;
 margin-top: 40px;
 white-space: nowrap;
 font: 400 15px Arial, sans-serif;
}

@media (max-width: 991px) {
 .meta-data {
  white-space: initial;
 }
}

.description {
 color: #000;
 text-align: center;
 align-self: stretch;
 margin-top: 67px;
 font: 400 16px/26px Arial, sans-serif;
}

@media (max-width: 991px) {
 .description {
  max-width: 100%;
  margin-top: 40px;
 }
}

.read-more {
 color: #fff;
 text-align: center;
 white-space: nowrap;
 border-radius: 4px;
 background-color: #333;
 align-self: center;
 margin-top: 40px;
 justify-content: center;
 padding: 20px 54px;
 font: 400 15px Arial, sans-serif;
}

@media (max-width: 991px) {
 .read-more {
  white-space: initial;
  padding: 0 20px;
 }
}

One of the easiest use cases is when you want to style a parent element when it has a certain child.

Let’s say we want to style our blog post differently when we have a subtitle. In that case, we’ll change the background color.

This is all the CSS code we’d need to add:

.header:has(h2) {
 background-color: darkgrey;
}

/* added style just to center the subtitle */
.subtitle {
 text-align: center;
}

Now if we add an h2 element with the subtitle class in our HTML:

<div class="main-container">
 <header class="header">
  <img loading="lazy" srcset="..." class="image" />
  <h1 class="title">Blog Post Title</h1>
  <h2 class="subtitle">Subtitle</h2>
  <div class="meta-data">Written by John Doe on October 10, 2022</div>
 </header>
 <p class="description">Lorem ipsum dolor sit amet...</p>
 <a href="#" class="read-more">Read More</a>
</div>

We will get a different color header:

Untitled

How about when we want to style the parent when it has a subtitle and a quote? Pretty straightforward:

.header:has(h2):has(blockquote) {
 background-color: hotpink;
}

Only when both h2 and blockquote will the background color change.

In case you might want to keep the styles when you have one or both elements:

.header:has(h2, blockquote) {
 background-color: lightsalmon;
}

Using the :not() pseudo-class we can do the opposite and set a style when a child element is not present in the parent:

.header:not(:has(h2)) {
 background-color: lightpink;
}

It’s possible to use :has() to select almost anything anywhere depending on a condition. In the following example, we’re even triggering a setting with no JS!

<body>
 <p>What is the meaning of life?</p>
 <p class="answer">42</p>
 <label>
  <input type="checkbox" class="blur-answer" />
  Hide answer
 </label>
</body>
body:has(input.blur-answer:checked) .answer {
 filter: blur(5px);
}

Once we check the input the answer class is blurred out.

What the selector is saying is that when the body element has an input with the class of blur-answer inside of it and it’s checked, style the following (any selector can be used).

If you use this method, don’t forget to make sure this is accessible!

Another use case for the “anywhere selector” could be to remove the logo from a nav bar in case the logo already appears in your hero section.

To show this off, I’ve used the generated nav bar from my colleague's post “Active Navlink Transitions with HTML, CSS and JavaScript”, took the logo from there, and generated a hero section that also has the logo.

This is what it looks like:

Untitled

The initial code is in this Codepen link.

In this case, the logo being duplicated is an eyesore, and your designer is going to throw a fit.

Luckily, we have an easy fix for this!

All we have to do is write a CSS selector that checks if we have a hero section (our .hero-with-logo class) and if that’s the case, hide the nav bar logo:

/* ... other styles */

body:has(.hero-with-logo) .nav-bar .logo {
/* This can be "display: none;", however in this case that would shift the links to the left */
 opacity: 0;
}

And now we have our logo only once:

Untitled

💡 All the code can be found here.

All the code can be found here.

This is something that was not possible before we had :has().

Let’s have a gander:

<p>element 1</p>
<p>element 2</p>
<p class="select-before">element 3</p>
<p>element 4</p>
<p>element 5</p>
*:has(+ .select-before) {
 background-color: palegoldenrod;
}

We have 5 elements, and on element 3 we’ve put the .select-before class. In our CSS we’re telling the browser to give a background color to any element that has a next-sibling which has the class select-before.

That way, element 2 will get a background color:

Untitled

Wait a minute for that to sink in…🤯

There’s a lot of whacky stuff we can do with this method, however, one that might be useful is highlighting a label of an invalid input:

<label for="url-input">Please enter a URL:</label>
<!-- when setting the "url" type your browser will handle validation and the user-invalid state  -->
<input type="url" id="url-input" />

<style>
label:has(+ #url-input:user-invalid) {
 border: 1px solid red;
}
</style>

With those 4 lines of code, we get native HTML validation with conditional styling and no JS!

This is what the experience looks like:

How cool is that?

Let’s say we use HTML and CSS for creating slide decks. According to the content on the slide, we can switch up the layout of the slide. For example:

<article>
 <h1>Hello slide with h1!</h1>
</article>

<article>
 <h2>Hello slide with h2 and img!</h2>
 <img class="img1" src="https://picsum.photos/600" />
</article>

<article>
 <h2>Hello slide with h2 and 2 imgs!</h2>
 <img class="img2" src="https://picsum.photos/200" />
 <img class="img3" src="https://picsum.photos/200" />
</article>
/* ... general styles  */

article:has(> is:(h1, h2):only-child) {
 grid-template-areas:
  "heading heading"
  "heading heading";
}

article:has(h2 + img) {
 grid-template-areas:
  "heading heading"
  "image1 image1";
}

article:has(h2 + img + img) {
 grid-template-areas:
  "heading heading"
  "image2 image3";
 place-content: center;
}

For the full code, check out the Codepen.

We’re changing the template areas according to the content.

Granted, this might become cumbersome if you have a lot of layout variations, but still, it’s a nice use case.

I’ve already shown you one way to do this in the previous sibling selector section, but that is in case a label and an input are siblings.

This is a bit more of a generic approach.

For example, we can style labels inside a form when there’s invalid user input:

<form>
 <div>
  <label for="pass">Password:
   <input id="pass" type="password" minlength="8" required/>
  </label>
 </div>
 <div>
  <label for="website">Website:
   <input id="website" type="url" required/>
  </label>
 </div>
 <button>submit</button>
</form>
label:has(input:user-invalid) {
 background-color: red;
}

Notice that both inputs are required and are nested within the label. This is what indicates to our selector that the label element that has an input in it is invalid.

Now if we don’t type in a password that matches the requirement of 8 characters or we don’t type a valid URL in the Website input, we’ll get an validation for it:

Again, all the UX interaction, none of the JavaScript!

Gotta love it.

We can also use this approach to style a when one of our inputs is invalid:

<form>
  <fieldset>
    <legend>Add your info</legend>
    <div>
  <label for="pass">Password:
   <input id="pass" type="password" minlength="8" required />
  </label>

 </div>
 <div>
  <label for="website">Website:
   <input id="website" type="url" required />
  </label>
 </div>
 <button>submit</button>
  </fieldset>
</form>
fieldset:has(input:user-invalid) {
 border: 1px solid red;
}

Check out how this behaves below:

Similarly to the label example, the key part is that the fieldset has the invalid input inside of it, to achieve this.

What if you want to style the siblings that we are not hovering?

Check it out:

<ul class="card-list">
 <li class="card">Card1</li>
 <li class="card">Card2</li>
 <li class="card">Card3</li>
 <li class="card">Card4</li>
 <li class="card">Card5</li>
</ul>
/* ... general styles */

.card-list:has(.card:hover) .card:not(:hover) {
 opacity: 0.4;
 scale: 0.9;
}

We’re targeting the card-list class, and checking whether a card is hovered, then selecting the cards we are not hovering.

This one is pretty cool and was not achievable in pure CSS before :has().

Another use case could be hovering over a table row and dimming out the other rows.

Style in case a parent has X or more children, we can style it differently. For example:

<ul class="card-list">
 <li class="card">Card1</li>
 <li class="card">Card2</li>
 <li class="card">Card3</li>
</ul>
/* ...styles */
.card-list:has(> *:nth-child(3n)) {
 display: grid;
 grid-template-columns: 1fr 1fr;
 place-items: center;
 gap: 1rem;
 background-color: lightgrey;
 & :last-child {
  width: 50dvw;
  background-color: royalblue;
  grid-column: 1 / -1
 }
}

This would result in the following:

Untitled

Have you ever had a case where your React component may have rendered an empty child, but you had a hard time noticing?

Try this out:

<article>
 <p>I am a post. If I have empty children my parent will have a border.</p>
 <p></p>
</article>
article:has(> *:empty) {
 height: auto;
 border: 1px dotted crimson;
}

This can come in handy when debugging.

In case you have a menu that has a dropdown with nested elements, you can find it and mark it. I’ve found this Codepen of a simple nested CSS-only menu, and all I had to do was add the following:

.menu-dropdown li:has(ul) b:after {
  content: " ⬇️"
}

Now, we know what section can expand:

Say you have a .png image that has a transparent background, you can find it and add a background color like so:

<main class="display">
 <img src="image.png" />
</main>
.display:has(img[src$=".png"]) img {
 background: linear-gradient(90deg, rgba(232,231,232,1) 0%, rgba(201,204,208,1) 100%);
}

You can use this method to grab into basically any attribute you can think of and style either the parent or any child of that parent. Feel free to come up with your use cases.

In the grand scheme of web development, the introduction of the :has() pseudo-class is like discovering a new continent on the map of CSS.

It's revolutionary, folks!

This nifty selector isn't just a minor update; it's a game-changer that opens up a world of possibilities for dynamic styling that we could only dream of before.

From styling parents based on their children's attributes to implementing complex layout changes without a single line of JavaScript, :has() brings the kind of power and flexibility that makes both designers and developers do a happy dance.

As we've explored, whether it's styling elements based on their quantity, selecting previous elements, or even handling form validations with a newfound elegance, :has() stands as a testament to the evolving nature of CSS.

It's like having a Swiss Army knife in your CSS toolbox, ready to tackle a myriad of challenges with precision and efficiency.

The introduction of :has() is a reminder of the ever-evolving landscape of web development. It's a call for all of us in the field to stay curious, keep experimenting, and continually push the boundaries of what's possible.

So, embrace the :has() pseudo-class, experiment with its capabilities, and watch as your styling game reaches new heights of creativity and efficiency.

The future of CSS just got a whole lot brighter, and I can't wait to see the innovative ways this community will leverage this powerful tool.

Happy coding, and may your styles be ever dynamic and responsive!

Introducing Visual Copilot: convert Figma designs to high quality code in a single click.

Try Visual Copilot

Share

Twitter
LinkedIn
Facebook
Hand written text that says "A drag and drop headless CMS?"

Introducing Visual Copilot:

A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot
Newsletter

Like our content?

Join Our Newsletter

Continue Reading
AI5 MIN
Introducing Visual Copilot 2.0: Design to Interactive
October 31, 2024
Design Systems8 MIN
Design Systems Explained
October 18, 2024
Visual Editing7 MIN
Visual editing is bridging the gap between developers and designers
October 11, 2024