DEV Community

Jane Ori
Jane Ori

Posted on

CSS-Only Auto-Follow Nav Highlighting

It's fairly common for large pages with multiple sections (documentation sites for example) to have a floating nav that highlights the item corresponding to what section you're scrolled to. With :has() we can now do this without JavaScript and with another CSS trick, we can make it stick too!

demonstration of auto-follow nav highlighting

Nav:has(Auto-Follow) Basic CSS Only Implementation

At its core, the idea is your pointer will enter the section/article you're reading and we can use :hover state of that section in a :has() selector to highlight the corresponding nav item. Like so:

This will only work in browsers with :has() support so here's a gif of what it looks like:

CSS-Only Auto-Follow Nav Highlighting Demo

The Selector in English

nav:has(+ main section:nth-of-type(1):hover) li:nth-of-type(1)
Enter fullscreen mode Exit fullscreen mode

Select the first li in a nav when the nav has a sibling element + main whose first section is being hovered. Then set the styles for the highlighted li.

In this case, the styles are triggered by a space toggle being flipped because it helps set up what comes next. If you're happy with the functionality as-is, you can just set the highlighted background and color there directly instead.

Nav:has(Auto-Follow) Advanced CSS Only Implementation

Ideally we would maintain the highlighted nav item until we've entered another section. Thanks to the "floodgate" animation-state trick this is also possible with just CSS!

Note: Floodgate requires animating a custom property which is part of the Houdini spec. So in addition to :has() support, at the time of writing, the only browser this full technique works in is Chrome Canary because it has implemented both Houdini and :has().

Here's a live demo:

And here's what it looks like in a gif:

CSS-Only Auto-Follow Nav Highlighting Demo With Sticky State

What Changed in the CSS

Most of the small additions are a straight copy-paste from the floodgate article but here's a recap:

@keyframes memory {
  0% { --cell: initial }
  1%, 100% { --cell: ; }
}
Enter fullscreen mode Exit fullscreen mode

First animation keyframes that toggles a memory cell space toggle, which is then assigned to the li's in our nav in the paused state with fill mode forwards:

  animation: memory 1ms linear 1 forwards paused;
Enter fullscreen mode Exit fullscreen mode

Then, where we were setting our highlighted styles previously with the :has() selector, we now un-pause the memory animation.
--cell: ;
becomes
animation-play-state: running;
which is our animation that does --cell: ; for us but remembers the state because when we stop hovering, the animation is paused again but now in its filled forward state.

Then our last change is resetting our memory on a specific nav li (clearing the highlight) only when we've hovered a different section with animation: none;. To do that, we have to write another set of :has() selectors which are a little bit harder to read...

Our New Selectors in English

The code:

nav:has(+ main :is(
  section:hover ~ section:nth-of-type(2),
  section:nth-of-type(2) ~ section:hover
)) li:nth-of-type(2)
Enter fullscreen mode Exit fullscreen mode

Starting out as a copy-paste from our previous selector, what's inside :has() following + main is the only change.

In English, select the nth li in a nav when the nav has a sibling element + main whose nth section is either a sibling section to a :hover'd section, OR our nth section is followed by a hovered section. If those conditions are met, UN-set the styles for the highlighted li (clear the memory animation).

We don't have to check for previous hovered sections for the first section, so that selector is simplified to only checking sections after:

nav:has(+ main section:nth-of-type(1) ~ section:hover) li:nth-of-type(1)
Enter fullscreen mode Exit fullscreen mode

similarly, our last section does not need to look for hovered sections after itself so it only looks before:

nav:has(+ main section:hover ~ section:nth-of-type(4)) li:nth-of-type(4)
Enter fullscreen mode Exit fullscreen mode

The End!

I've personally used this same logic in JS a few times (highlighting a corresponding detail, locking that state, and clearing the previous), so I imagine this CSS pattern will be a useful idea in many other situations too.

If you think this idea/trick is neat, it's the kind of thing I do all the time! So please do consider following me here and on twitter as well!

💜
// Jane Ori

Top comments (7)

Collapse
 
afif profile image
Temani Afif

There is a small glitch in the last demo, if you hover a previous element you will have two (or more) tabs highlighted at the same time

Collapse
 
janeori profile image
Jane Ori

Did you edit the pen locally? That only happens if you remove/change the second set of has selectors. Works as expected (as shown in the gif recording) in latest Chrome Canary

Collapse
 
afif profile image
Temani Afif

no, I am trying your codepen with no edits

glitch

Thread Thread
 
janeori profile image
Jane Ori • Edited

Neat! Browser bug - it's not clearing the animation for you.
What version of Canary? Mine is latest - 105.0.5147.0 - on windows
edit: also works as expected on canary 105.0.5132.0 on mac

Thread Thread
 
afif profile image
Temani Afif

actually I am not using Canary but the last Chrome version with Flag enabled (on Ubuntu) but I guess there are a lot of stuff in Dev so things might change rapidly.

Collapse
 
geforcesong profile image
George Guo

not working at all

Collapse
 
janeori profile image
Jane Ori

will only work in browsers with :has() support


Note: Floodgate requires animating a custom property which is part of the Houdini spec. So in addition to :has() support, at the time of writing, the only browser this full technique works in is Chrome Canary because it has implemented both Houdini and :has().


I should have emphasized the text in the article more but here it is again. Thanks for checking it out in any case