If you’re writing documents or blog posts, it’s sometimes desirable to make a list of ‘anchor links’ that jump a reader to a different section of the document. Perhaps it’s easier to think of this pattern as a table of contents.

If you’re reading this article on a wide enough screen, you’ll see this kind of thing over on the right. Click a link and you get scooted down to the relevant heading.

No-one wants to make these things manually and, as they aren’t essential to the understanding of the document, I always make them with a little JavaScript snippet.

If you’re a beginner with JavaScript, it may interest you to understand how it works. Despite being relatively simple, we will be using a number of ES6 (latest JavaScript) language features: de-structuring, arrow functions and template literals.

Let’s go!

Approach

Before writing a line of code it is important to think through, conceptually how this might be achieved. Initially I thought about it like this: “find every header, make a link to that header and stick all of those links into a single container”.

Then I considered that I might want to limit where I search for any headers. It might also be beneficial to change where I placed the container in the DOM. Fleshing it out a little more, here is my layman’s terms approach. “Find every header within a given target area, make a link to each header and stick all of those links into a single container, then place that container into a given area”. That sounded good enough to make a start.

Function arguments

If we want this snippet of code to be reusable, it needs to facilitate options. Historically, I’ve written options, and their default values, for functions like this:

function TOC(options) {
    var appendInto = options.appendInto || "body";
    var headerScope = options.headerScope || "body";
    var containerClass = options.containerClass || "toc-Wrapper";
    var linkClass = options.linkClass || "toc-Linkc";
    var hTagsToLink = options.hTagsToLink || "h1,h2";
    // Rest of function
}

// Call that function with some options
TOC({
    appendInto: "#intro",
    containerClass: "toc-Wrapper",
    linkClass: "toc-Link",
    hTagsToLink: "h1,h2",
});

With that approach we set our function up to accept a single parameter. We want that parameter to be an object. Then inside the function we ‘wire up’ the various values of the object to variables that we can then use inside the function.

When we invoke the function we pass an object to it (everything in the curly braces). The function uses those values unless one isn’t supplied, in which case it uses the alternate value (specified with the bit after the or || for each variable).

Function arguments – ES6 style

ES6 provides object de-structuring. Object de-structuring has many use-cases, but here we will use them to provide default function parameters.

I’m not going to explain the in’s and out’s of using object de-structuring for this use-case. Instead I am going to refer you to this excellent post on the subject: https://simonsmith.io/destructuring-objects-as-function-parameters-in-es6/.

Instead, compare that prior example to an ES6 version:

function TOC({ appendInto = "body", headerScope = "body", containerClass = "toc-Wrapper", linkClass = "toc-Link", hTagsToLink = "h1,h2" } = {}) {
    // Rest of function
}

One important thing to note here. Notice how the entire object is made optional with the = {} at the end. Without that, calling the function without passing an object, e.g. TOC() wouldn’t work.

Entire function walk-through

Let’s look at that the whole function now, and then we can consider how that achieves the original approach:

function TOC({ appendInto = "#intro", headerScope = "body", containerClass = "toc-Wrapper", linkClass = "toc-Link", hTagsToLink = "h1,h2" } = {}) {
    let jsNav = document.createElement("nav");
    jsNav.classList.add(containerClass);
    let appendArea = document.querySelector(appendInto);
    let hTags = document.querySelector(headerScope).querySelectorAll(hTagsToLink);
    hTags.forEach((el, i) => {
        el.id = `h-${el.tagName}_${i}`;
        let link = document.createElement("a");
        link.setAttribute("href", `#h-${el.tagName}_${i}`);
        link.classList.add(linkClass);
        link.classList.add(`${linkClass}_${el.tagName}`);
        link.textContent = el.textContent;
        jsNav.appendChild(link);
    });
    appendArea.appendChild(jsNav);
}

We create a nav element and add a class to it. Because of how we set up the options, even if we don’t pass something specific it will get the default class. Next we grab a reference to where we want to add this list of anchor links. Again if not specified we have a default.

Then we want to find all the heading tags on the page:

let hTags = document.querySelector(headerScope).querySelectorAll(hTagsToLink);

That might look a little complicated but it’s just building up a CSS selector from what we provide. For example, if we invoke the function like this:

TOC({
    appendInto: "#intro",
    containerClass: "toc-Wrapper",
    linkClass: "toc-Link",
    hTagsToLink: "h1,h2",
});

We haven’t provided a headerScope so it will become the default: body. So, in this instance, the line is effectively evaluating to:

let hTags = document.querySelector("body").querySelectorAll("h1,h2");

Iterating with an array method and arrow function

So, we now have all the header tags we want in the hTags variable. We will use an array method to loop through them all and create a link element for each of them. In the code block below, we’re using an arrow function but obviously a standard ES5 function would work just as well. Either way, we have the el which is a reference to the element we are iterating over and the second parameter, i is the iteration count (if it’s on the third thing it will evaluate to 2 as it is zero-indexed).

Create an name for ids and links with template literals

In terms of what we are doing on each iteration. First we add an id to the header tag we are iterating over with a template literal.

el.id = `h-${el.tagName}_${i}`;

It looks a bit funky but if you think about the third tag found, if a h3 it would create an id of h-H3_2.

This same pattern is used on the anchor links to wire them up too. We create an a anchor tag, set the href to the aforementioned pattern, add a couple of classes (one the same as all the others, one specific to this iteration, just in case it may be useful). Then we set the text of the link to be the same as the header tag. Finally we append each link into the nav element made outside the loop/forEach.

hTags.forEach((el, i) => {
    el.id = `h-${el.tagName}_${i}`;
    let link = document.createElement("a");
    link.setAttribute("href", `#h-${el.tagName}_${i}`);
    link.classList.add(linkClass);
    link.classList.add(`${linkClass}_${el.tagName}`);
    link.textContent = el.textContent;
    jsNav.appendChild(link);
});

Append it where you want it!

Finally, we append the nav element into the relevant place in the DOM:

appendArea.appendChild(jsNav);

Smooth scrolling with one line of CSS

Nowadays, Firefox and Chrome support smooth scroll behaviour, so make those anchor links behave a little nicer with this one-liner in CSS:

html {
    scroll-behavior: smooth;
}

Summary

That’s all there is to it. A few lines of JavaScript and we have a flexible anchor tag creating snippet. More importantly, if you’ve followed along you will have a handle on object de-structuring, arrow functions and template literals. Each of which is a great addition to the JavaScript language.

My latest book, 'Enduring CSS' is out now

Get $5 off HERE ↠

Write and maintain large scale modular CSS and embrace modern tooling including PostCSS, Stylelint and Gulp