Simple Table-of-Contents Highlighting Stimulus Controller

Jon Sully

1 Minute

Sharing a bit of code to highlight some links!

Just a quick post here to share some code. I wanted to add subtle highlighting (or bolding, really) to my Table of Contents as the reader scrolls through the page:

A screenshot of the table of contents this site uses in desktop mode with one header bolded as it’s on screen
So bold ✨ so fresh

So after a Google-search found nothing out there, I whipped up an ultra-simple Stimulus controller to do this for me.

First, we need a couple of bindings in the HTML. These two go on the upper-most <section> of my blog.html.erb layout:

data-controller="toc-highlight" data-action="scroll@window->toc-highlight#updateHighlight"

But can sort of go anywhere (as long as it’s hierarchically above the TOC itself) since everything operates via a window-level event binding and the following target. Since my TOC is generated from the Markdown and injected via Redcarpet, I simply add the target to the parent container like so:

<div data-toc-highlight-target="links">
  <%= toc(post).html_safe %>
</div>

And that’s it. Now we just need the simple Stimulus code itself:

// app/javascript/controllers/toc_highlight_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["links"]

  updateHighlight() {
    const links = [...this.linksTarget.getElementsByTagName("a")]
    let highlightedLink = null

    links.forEach(link => {
      const id = link.getAttribute("href").slice(1)
      const section = document.getElementById(id)
      const rect = section.getBoundingClientRect()

      // Check if the section's top is past half the window height
      if (rect.top <= window.innerHeight / 2) {
        highlightedLink = link
      }
    })

    // If a link is to be highlighted, remove bold from others and highlight this one
    if (highlightedLink) {
      links.forEach(link => link.classList.remove("font-bold"))
      highlightedLink.classList.add("font-bold")
    }
  }
}

Pretty straightforward! Any time a header scrolls past the half-way mark of the window, it gets bolded!

Feel free to check out this blog post to see it in action.

✌️

Comments? Thoughts?

Please note: spam comments happen a lot. All submitted comments are run through OpenAI to detect and block spam.