CSS-Only Auto-Follow Nav Highlighting
Tags: css, tutorial, webdev, codepen
Published: 2022-06-28 03:39:15

It's fairly common for large pages with multiple sections ([documentation sites for example](https://donejs.com/Apis.html#can-definemapmap)) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nc4vfsic50aeu6su6ba1.gif)

## 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:

{% codepen https://codepen.io/propjockey/pen/30ff76c039f9aa99cfe97eb81f573240 default-tab=css,result theme-id=40148 %}

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zroemu6upmh79eil7z2j.gif)

### The Selector in English

```css
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](https://github.com/propjockey/css-sweeper#css-is-a-programming-language-thanks-to-the-space-toggle-trick) 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](https://dev.to/janeori/an-innovative-idea-for-holding-state-in-css-23gh) 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:

{% codepen https://codepen.io/propjockey/pen/2e80d625c052c24fe57847c398709df0 default-tab=css,result theme-id=40148 %}

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

![CSS-Only Auto-Follow Nav Highlighting Demo With Sticky State](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rp2yu74ow259q9tsr6tn.gif)

### What Changed in the CSS

Most of the small additions are a straight copy-paste from [the floodgate article](https://dev.to/janeori/an-innovative-idea-for-holding-state-in-css-23gh) but here's a recap:

```css
@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:

```css
  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:

```css
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:
```css
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:
```css
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](https://twitter.com/Jane0ri) as well!

💜
// Jane Ori