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!
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:
The Selector in English
nav:has(+ main section:nth-of-type(1):hover) li:nth-of-type(1)
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:
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: ; }
}
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;
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)
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)
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)
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)
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
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
no, I am trying your codepen with no edits
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
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.
not working at all
I should have emphasized the text in the article more but here it is again. Thanks for checking it out in any case