Using :has as a previous sibling selector

·

3 min read

I got very excited this week when I finally used :has. The trouble with it has been browser support, which isn't yet high enough to use it in production code. In particular, it's not going to be in Firefox until 121 (finally!) and that's my favourite browser...

But this week I had an occasion to use it. This was because it was only an aesthetic change, so it won't matter if it doesn't work for people on older browsers. And also because the project I'm using it on has won't be launching for a few months, by which time it'll work in a lot more browsers.

Here is the scenario: I have a list of links. When I hover over any of them, the rest should go a bit transparent. But if I'm not hovering over any, none of them should go transparent.

What makes this slightly tricky is that the links should all have an opacity of 1 by default and only change when another link is hovered over.

The HTML

Here's some HTML I have set up for an example:

<ul>
  <li><a href="#">One</a></li>
  <li><a href="#">Two</a></li>
  <li><a href="#">Three</a></li>
  <li><a href="#">Four</a></li>
  <li><a href="#">Five</a></li>
  <li><a href="#">Six</a></li>
</ul>

Next siblings selector

Since the link is a child of the list item, the simplest thing to do is to put the hover on the list item. If I'm hovering on the link then by definition I'm hovering on the parent of the link.

Targeting the siblings that come after the item we're hovering on is easy:

li:hover ~ li {
  opacity: 0.5;
}

To do the same for the siblings that come before the one we're hovering on involves using :has and effectively writing the same thing, but backwards:

li:has(~ li:hover) {
  opacity: 0.5;
}

This feels so weird to read.

The first snippet here is saying: Apply the CSS to any li that is a (next) sibling of the one being hovered over.

The second snippet here is saying: Apply the CSS to any li that has a (next) sibling being hovered over.

Focusing

Applying something on hover is all very well and good, but what if you always want it to apply on focus? Here we can use focus-within: so if something within the li has focus, apply the CSS. The li can't have focus (unless you add tabindex to it) and the link is the only thing inside it that can be focused.

li:hover ~ li,
li:has(~ li:hover),
li:focus-within ~ li,
li:has(~ li:focus-within) {
  opacity: 0.5;
}

Simple - it's just the same code, but using focus-within instead of hover.

There is a problem with this when one of the links has focus, but you hover on another: everything goes a bit transparent. But arguably, how many users are going to do that? Also arguably, do you need the same effect on focus-within when the link you're focused on has an outline around it?

The final code

Here is the whole code in CodePen, so you can see it in action and have a play (as long as you're using a browser that supports :has, of course):