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.
Prev: CSS Gradient text