How to Automatically Generate Table of Contents from Headings

Santaji GadeJavaScript2 days ago8 Views

generate table of contents

Automatically generate table of contents from headings using vanilla JS — no library needed. Slugified IDs, nesting, and active-section highlighting.

Technical SEO Table of Contents JavaScript 2026

Automatically generating a table of contents from headings comes down to three steps: query every heading in your content, generate a unique, URL-safe ID for each one, then build a linked list pointing to those IDs. No framework or library is required, vanilla JavaScript handles this in under 40 lines.

Auto Generate Table of Contents

A dynamic table of contents stays in sync with your content automatically, so adding or removing a section never leaves a stale, hand-maintained list behind.

Here's the working code for basic generation, collision-safe ID slugification, and active-section highlighting using IntersectionObserver.

2+
headings typically recommended as the minimum before generating a TOC
0
external libraries required for a working vanilla JS implementation
3.6KB
minified size of Tocbot, a popular dependency-free reference implementation
Advertisement
Advertisement

01Basic Table of Contents Generator

Query headings with a scoped selector to avoid pulling in unrelated headings from your header, sidebar, or footer. This example targets only headings inside a content container.

Basic TOC Generator (Vanilla JS)
function generateTOC(contentSelector, tocSelector, levels = 'h2, h3') {
  const content = document.querySelector(contentSelector);
  const tocContainer = document.querySelector(tocSelector);
  const headings = content.querySelectorAll(levels);

  if (headings.length < 2) return; // Skip TOC on short articles

  const list = document.createElement('ol');

  headings.forEach((heading, index) => {
    if (!heading.id) {
      heading.id = `section-${index}`;
    }

    const listItem = document.createElement('li');
    const link = document.createElement('a');
    link.href = `#${heading.id}`;
    link.textContent = heading.textContent;

    listItem.appendChild(link);
    list.appendChild(listItem);
  });

  tocContainer.appendChild(list);
}

generateTOC('.article-content', '#toc');

02Collision-Safe, URL-Friendly IDs

Auto-generated IDs like "section-0" work but aren't meaningful in a URL. Slugifying the actual heading text creates readable anchors, and a duplicate counter prevents two identical headings from colliding on the same ID.

Slugify Heading Text Into a Safe ID
function slugify(text) {
  return text
    .toLowerCase()
    .trim()
    .replace(/[^\w\s-]/g, '')   // Remove special characters
    .replace(/[\s_]+/g, '-')    // Replace spaces/underscores with hyphens
    .replace(/^-+|-+$/g, '');   // Trim leading/trailing hyphens
}

function assignUniqueIds(headings) {
  const usedIds = new Set();

  headings.forEach(heading => {
    if (heading.id) {
      usedIds.add(heading.id);
      return;
    }

    let baseId = slugify(heading.textContent);
    let finalId = baseId;
    let counter = 1;

    // Handle duplicate headings by appending a counter
    while (usedIds.has(finalId)) {
      finalId = `${baseId}-${counter}`;
      counter++;
    }

    heading.id = finalId;
    usedIds.add(finalId);
  });
}
Advertisement
Advertisement

03Accessible, Nested Table of Contents

Wrap the generated list in a nav element with an aria-label to create a proper accessible navigation landmark. This version also nests h3 headings under their parent h2, matching visual and semantic hierarchy.

Nested, Accessible TOC With Semantic Hierarchy
function generateNestedTOC(contentSelector, tocSelector) {
  const content = document.querySelector(contentSelector);
  const tocContainer = document.querySelector(tocSelector);
  const headings = content.querySelectorAll('h2, h3');

  if (headings.length < 2) return;

  assignUniqueIds(headings); // From the slugify function above

  const nav = document.createElement('nav');
  nav.setAttribute('aria-label', 'Table of contents');

  let currentList = document.createElement('ol');
  let currentSubList = null;
  nav.appendChild(currentList);

  headings.forEach(heading => {
    const link = document.createElement('a');
    link.href = `#${heading.id}`;
    link.textContent = heading.textContent;

    const listItem = document.createElement('li');
    listItem.appendChild(link);

    if (heading.tagName === 'H2') {
      currentList.appendChild(listItem);
      currentSubList = document.createElement('ol');
      listItem.appendChild(currentSubList);
    } else if (currentSubList) {
      currentSubList.appendChild(listItem);
    }
  });

  tocContainer.appendChild(nav);
}

04Highlighting the Active Section on Scroll

IntersectionObserver is the modern, efficient way to highlight the active section as a reader scrolls, avoiding the performance cost of listening to scroll events directly.

Active Section Highlighting (IntersectionObserver)
function highlightActiveSection(contentSelector, tocSelector) {
  const headings = document.querySelectorAll(`${contentSelector} h2, ${contentSelector} h3`);
  const tocLinks = document.querySelectorAll(`${tocSelector} a`);

  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        tocLinks.forEach(link => link.classList.remove('active'));
        const activeLink = document.querySelector(`${tocSelector} a[href="#${entry.target.id}"]`);
        if (activeLink) activeLink.classList.add('active');
      }
    });
  }, { rootMargin: '0px 0px -70% 0px' });

  headings.forEach(heading => observer.observe(heading));
}

highlightActiveSection('.article-content', '#toc');
Advertisement
Advertisement

05Approach Comparison

A quick reference for choosing between a custom script and an off-the-shelf library.

ApproachBest ForTrade-off
Custom vanilla JS (above)Full control, no dependencies, small pagesYou maintain the code yourself
Tocbot (library)Documentation sites, scrollspy support built inSmall added dependency (~3.6KB)
jQuery TOC pluginsLegacy sites already using jQueryRequires jQuery as a dependency
CMS-native TOC blocksWordPress/CMS users wanting zero codeLess customizable than a hand-written script

06Practical Notes Before Implementing

A few things worth confirming before shipping any TOC script to production.

Scope your heading query, use a content container selector, not a bare document.querySelectorAll on the whole page.

Skip TOC generation on short pages, most implementations abort below 2 headings to avoid cluttering brief content.

Slugify heading text for IDs, readable anchors help both users and crawlers understand page structure.

Handle duplicate heading text, a counter suffix prevents ID collisions across repeated section titles.

Wrap the output in a nav with aria-label, this creates a proper accessibility landmark for screen readers.

07Common Questions

No. Vanilla JavaScript can generate a fully working, accessible table of contents in under 40 lines, with no external dependencies required.

Without handling this, both would get the same slugified ID, breaking anchor links. A duplicate counter appends a number to the second occurrence to keep IDs unique.

No. Most implementations skip generation on pages with fewer than two headings, since a table of contents adds clutter without real navigational value on short content.

Use IntersectionObserver rather than a scroll event listener. It's more performant and lets you detect which heading is currently in view without constant recalculation.

Yes. Track the current top-level list item while iterating headings, and append any subsequent lower-level heading (like h3) into a nested list under it.

What We Learn Today

A working TOC needs no library, just query, ID, and link

Slugified text makes anchors readable and meaningful

A duplicate counter prevents ID collisions on repeated headings

Nav with aria-label creates a proper accessibility landmark

IntersectionObserver beats scroll listeners for active highlighting

Most implementations skip TOC generation below 2 headings

Build a Complete Technical SEO Toolkit

Table of contents generation pairs well with broken link detection and canonical tag setup. Explore both guides next.

0 Votes: 0 Upvotes, 0 Downvotes (0 Points)

Leave a reply

Loading Next Post...
Search
Popular Now
Loading

Signing-in 3 seconds...

Signing-up 3 seconds...