Go to main content

Table of contents in Javascript

In this post, I will provide you with a javascript code sample that transforms headings in a page into a structured table of contents with anchor links.

Table of contents in Javascript

When writing a long structured article, it is always handy for your readers having a table of contents that helps them navigate through it. I composed a javascript code sample that creates such a table of contents on the fly for you. It iterates through the article headings and 

  • creates anchors for each of them and 
  • renders a structured unordered list with anchor links.

Here is a link to a working demo on Codepen.

How to make it work?

  1. In your markup, create an empty ul element (i.e. <ul class="table-of-contents"></ul>) that will stand for the table of contents. 
  2. Copy the Javascript code from the pen or code block below into your project. I wrote the code in ES6 so that you may need to transpile it into ES5 in case you need to use the functionality on the client side. Babel could be a great choice.
  3. When calling the tableOfContents function, pass the settings object with selectors for 
    • the headings you want to be part of the table of contents (headingsSelector property) and 
    • the table of contents wrapper you've created in the first step (wrapperSelector property).

The code

Working demo on Codepen 

const tableOfContents = (settings) => {
  // Helper function that iterates all headings and returns the highest level.
  // For example, if array of  h2, h3 and h4 is passed, the function returns 2.
  const getHighestHIndex = headings => {
    let indexes = Array.from(headings).map(item => {
      return parseInt(item.tagName.replace('H', ''));
    });
    return indexes.reduce((a, b) => { 
      return Math.min(a, b);
    });
  };

  // Iterates passed headings, gets their inner HTML and transforms it into id
  const createAnchors = (settings) => {
    let headings = document.querySelectorAll(settings.headingsSelector);
    headings.forEach(item => {
      let anchorName = item.innerHTML.toLowerCase().replace(/(<([^>]+)>)/ig,'').replace(/\W/g,'-');
      item.setAttribute('id', anchorName);
    });
  };

  // Iterates passed headings and creates unordered list with anchor links reflecting heading levels
  const createTableOfContents = (settings) => {
    let headings = document.querySelectorAll(settings.headingsSelector);
    let tableOfContentsWrapper = document.querySelector(settings.wrapperSelector);
    let tableOfContents = '';
    let prevHeadingLevel = getHighestHIndex(headings);
    headings.forEach(item => {
      let headingLevel = parseInt(item.tagName.replace('H', ''));
      if (prevHeadingLevel > headingLevel) {
        tableOfContents += '</ul>';
      }
      if (prevHeadingLevel < headingLevel) {
        tableOfContents += '<ul>';
      }
      tableOfContents += `<li><a href="#${item.getAttribute('id')}">${item.innerHTML}</a></li>`;
      prevHeadingLevel = headingLevel;
    });
    tableOfContentsWrapper.innerHTML = tableOfContents;
  };
  
  createAnchors(settings);
  createTableOfContents(settings);
};

// Init table of contents
tableOfContents({
  headingsSelector: 'h2:not(.do-not-render), h3, h4',
  wrapperSelector: '.table-of-contents'
});


Milan Lund

is a freelance web developer and a proud Basenji owner. His specialties are Kentico CMS/EMS and Kentico Cloud.

Further reading

all posts
  • Kentico CMS & EMS

    What is Kentico

    In this article you'll find a short summary about what Kentico is.