Dynamic table of contents with Intersection Observer

My Hugo theme had a jQuery-powered dynamic table of contents. I thought it didn’t make much sense to keep the jQuery library (even a small version) if I’m just using it for that. So, out it went, and in went a JavaScript version with Intersection Observer.

This is based on Ben Frain’s version, simplified a bit since we get the table of contents statically embedded.

Code

The full code (in layouts/partials/article.html) is:

// Intersection Observer Options
var myObserverOptions = {
    root: null,
    rootMargin: "0px",
    threshold: [1],
};

// Each Intersection Observer runs setCurrent
var observeHtags = new IntersectionObserver(setCurrent, myObserverOptions);

// Add IO to all headings
function addIntersectionObserver() {
    var allHtags = document.querySelectorAll(".article-entry > h1, .article-entry > h2, .article-entry > h3");
    allHtags.forEach(tag => {
        observeHtags.observe(tag);
    });
}

// runs when the Intersection Observer is sent
function setCurrent(e) {
    e.map(i => {
        if (i.isIntersecting === true) {
            document.querySelector(`a[href="#${i.target.id}"]`).classList.add("active");
        } else {
            document.querySelector(`a[href="#${i.target.id}"]`).classList.remove("active");
        }
    })
}

(function setUp() {
    addIntersectionObserver();
    document.getElementsByClassName('article-toc')[0].style.display = '';
})();

(Github Gist)

There’s also a Glitch if you want to try it out simplified.

Older code

If you’re curious, the previous version is here.

Overview

Functionally, what happens is roughly:

  1. All h1, h2, h3 headings from the content are fetched
  2. An observer is added to them. This fires an event when the element becomes visible or disappears.
  3. Ad the observers fire, add the class active to each table of contents item

This isn’t perfect.

In particular if you’re reading a section where the heading is no longer visible, it doesn’t continue to highlight that section in the table of contents. A workaround could be to dynamically label all elements within the contents and add intersection observer to them. This is a bit annoying, and likely unnecessarily slows things down (constant events going off even for small scrolls).

Also, it doesn’t do the fancy opening / closing of subsections. I don’t really care for that, since my posts aren’t that long.

Does it work? Check out the sample page.

Comments / questions

There's currently no commenting functionality here. If you'd like to comment, please use Twitter and @me there. Thanks!

Tweet about this - and/or - search for latest comments / top comments

Related pages