Using CSS :has() to select the previous sibling

In this post, I'll walk through how we can achieve what was once impossible with CSS: selecting the previous sibling.

Problem

Before CSS introduced the :has() pseudo class, we would typically use Javascript to select previous siblings of a specific element. We could easily select the next sibling element using the next sibling combinator: + in CSS.

Demo

Lets say you have a menu, or a list of items, on your website and you want to have some effect/state applied when a user interacts with it. We want to make this fluid so the items surrounding the hovered element will also react to the interaction. This static demo showcases the final effect:

To achieve this we would write something like this in HTML, CSS and JS:

<div class="col-span-full mt-8 mb-16">
  <nav class="demo-menu flex w-full justify-evenly items-center uppercase text-sm">
    <a href="#">Home</a>
    <a href="#" class="active">About</a>
    <a href="#">Blog</a>
    <a href="#">Contact</a>
  </nav>
</div>
.demo-menu {
  opacity: 0.8;
}

.demo-menu a {
  text-decoration: none;
}

.demo-menu .active {
  opacity: 1;
  scale: 1.8;
}

.demo-menu .active-sibling,
/* Get the next sibling with CSS */
.demo-menu .active + a {
  scale: 1.2;
  opacity: 0.9;
}
const active = document.querySelector('.active');
const previousSibling = active?.previousElementSibling;
previousSibling?.classList.add('active-sibling');

// You can also use JS to get the next sibling
// const nextSibling = active?.nextElementSibling;
// nextSibling?.classList.add('active-sibling');

Recreating it with modern CSS

Now that CSS has the :has() pseudo class, we can re-create the above effect without any Javascript 🎉. The below demo works when you hover each menu link. You'll see that the previous and next siblings have the same styling when a link hovered:

<div class="col-span-full mt-8 mb-16">
  <nav class="demo-menu flex w-full justify-evenly items-center uppercase text-sm">
    <a href="#">Home</a>
    <a href="#">About</a>
    <a href="#">Blog</a>
    <a href="#">Contact</a>
  </nav>
</div>
.demo-menu {
  opacity: 0.8;
}

.demo-menu a {
  text-decoration: none;
  transition: all 0.3s ease-out;
}

.demo-menu a:hover {
  opacity: 1;
  scale: 1.8;
}

.demo-menu :hover + a,
.demo-menu a:has(+ a:hover)  {
  scale: 1.2;
  opacity: 0.9;
}

The HTML remains the same but I've removed the Javascript and added an additional line of CSS: .demo-menu a:has(+ a:hover):

.demo-menu :hover + a,
.demo-menu a:has(+ a:hover) {
  scale: 1.2;
  opacity: 0.9;
}

.demo-menu a:has(+ a:hover) is selecting any a element that has a next sibling that is currently being hovered. This could also be modified to look for a specific class too.

Browser support

This is now available in all modern browsers, but you may want to ensure there's backwards support by using @supports rule:

@supports selector(:has()) {
  .demo-menu :hover + a,
  .demo-menu a:has(+ a:hover) {
    scale: 1.2;
    opacity: 0.9;
  }
}

Wrap up

This is a very simple use case for the new :has() pseudo class. I really like that this new feature and it helps reduce the amount of JS hacks we've had in the front-end to achieve something quite simple.