Add a Simple Table of Contents to Your Website

Ever on a long article and wished you could jump around easily? Or even return to a section quickly? Want to provide this experience to your readers? I'll show you how you can quickly and easily add a table of contents to your website with a few lines of JavaScript.

Table of contents on grid paper

The goal for a table of contents is to provide your reader a guide and preview of your article's content and provide a way for them to navigate that content easily. This assumes that your content is already semantically structured.

What's Semantic Structure?

What does semantically structured mean? Philosophically, it means that your content is organized in a meaningful way. Practically, it means your content is organized by headings that use heading tags (like <h2>). These headings indicate the important sections of your article and break up large walls of text, which no one likes.

Heading tags begin with h1, which is usually reserved for the title of the article, and go down to h6. Best practices stipulate that an h3 tag must be preceded by an h2 tag so that each subheading is always preceded by a heading one level up. In other words, an h2 tag would define a major section of your article, while an h3 tag would define a subsection of that h2 section. Here's an example:

<h1>Food: A Tasty Article</h1>

	<h2>Fruits: Nature's Candy</h2>

		<h3>Oranges: Truth in Advertising</h3>	

		<h3>Apples: How Do You Like 'Em?</h3>

			<h4>Red Delicious: False Advertising</h4>
	<h2>Vegetables of Contents</h2>

		<h3>Lettuce End This Example</h3>
Example of semantic structure

The Advanced Solution

The solution I provide to create a table of contents is simple. It gets the job done.

However, if you're looking for a table of contents that is able to show the reader where they are in the article, is tried and tested against numerous edge cases, and does other stuff, then check out tocbot for all your TOC needs.

The Simple Solution

My implementation works by finding all of the headings in the article. The code assumes you have a class .article on your content's container. You'll need to update this class to whatever the actual class name is.

const articleContainer = document.querySelector('.article');
const headings = articleContainer.querySelectorAll('h2, h3, h4, h5, h6');
Selecting the headings

If the article has fewer than two headings, then the script aborts and doesn't create a table of contents. The assumption here is that you wouldn't want a TOC on a short article and that a longer article will have more than two headings. There may be edge cases where this behavior isn't desired, in which case you could modify the if check to fit your needs.

if (headings.length > 2) {
    // Create table of contents
Checking the number of headings

Create a Toggle

Next, we take advantage of the browser's native <details> element to create a nice, cross-browser-compatible dropdown toggle. The details element includes a <summary> tag where you can include the text that shows even when the toggle is closed.  The idea here is to hide the table of contents until it's wanted and not to needlessly obstruct access to your writing, especially in cases where your TOC is quite long.

We also add a class to the element to allow for styling, populate the summary element with text, and then attach the summary to the details tag.

const details = document.createElement('details');
details.setAttribute('class', 'toc-content'); // You can use any class name you want
const summary = document.createElement('summary');
summary.innerHTML = '<strong>Table of Contents</strong>';
Create the details toggle

Construct the TOC

The next step is to loop through the headings in the article and add them to the table of contents. In this step, four things happen:

  1. We extract the text from the heading and add it to the table of contents in a p tag
  2. We assign the table-of-content element a class based on the heading level (which will allow us to apply semantic styling like indents)
  3. We add a link to the table-of-content element to take the reader to that section of the article
  4. We add the TOC to the page
headings.forEach((el) => {
    const p = document.createElement('p');
    // Add a class to the p tag with the heading like "toc-h2"
    p.setAttribute('class', 'toc-' + el.tagName.toLocaleLowerCase());
    const a = document.createElement('a');
    a.setAttribute('class', 'toc-link');
    a.textContent = el.textContent;
    // Add a link to the section in the text
    a.href = '#' +;

// Add the table of contents to the beginning of the article  
Creating the table of contents and adding it to the page
Because the TOC is dynamically created on page load, it changes with your content. Added or deleted a section? The TOC will reflect that automatically. No tedious editing!

What if My Headings Don’t Have IDs?

To create the TOC, your heading must have an ID because it's used as the target for the anchor tag <a>. Many CMS's will generate this ID for you automatically. If yours doesn't, no worries–the solution is chef's-kiss simple.

headings.forEach((el) => {
	// Generate IDs if they don't exist = || el.textContent.toLocaleLowerCase().replace(/\W/g,"-");
    // Rest of the loop
Generate IDs

Here, we check whether the heading has an ID. If it does, then we use that. Otherwise, we use regex to replace all non-word characters (like spaces) with hyphens. These IDs are added to the heading element and used for the TOC to provide navigation.

Putting It All Together: The Code

const articleContainer = document.querySelector(".article");

const headings = articleContainer.querySelectorAll("h2, h3, h4, h5, h6");

if (headings.length > 2) {
   // Generate IDs if they don't exist - optional = || el.textContent.toLocaleLowerCase().replace(/\W/g,"-");
  const details = document.createElement("details");
  details.setAttribute("class", "toc-content"); // You can use any class name you want
  const summary = document.createElement("summary");
  summary.innerHTML = "<strong>Table of Contents</strong>";
  headings.forEach((el) => { = || el.textContent.toLocaleLowerCase().replace(/\W/g,"-"); // Create IDs if missing - optional
    const p = document.createElement("p");

    // Add a class to the p tag with the heading like "toc-h2"
    p.setAttribute("class", "toc-" + el.tagName.toLocaleLowerCase());
    const a = document.createElement("a");
    a.setAttribute("class", "toc-link");
    a.textContent = el.textContent;

    // Add a link to the section in the text
    a.href = "#" +;

  // Add the table of contents to the beginning of the article
Complete TOC code

The resulting TOC markup will look like the following:

<details class="toc-content">
        <strong>Table of Contents</strong>
    <p class="toc-h2">
        <a class="toc-link" href="#heading-id">Heading Name</a>
    <p class="toc-h3">
        <a class="toc-link" href="#heading-id-2">Heading Name 2</a>
    <!-- And so on -->
Example markup for TOC

Style the TOC

The final cherry on top of this TOC sundae is to style it. You can, of course, make it fit your site's aesthetic, but some default styles are shared below. They reduce the font size and line height of the TOC and add indentations based on heading level.

/* Reduce the size and line-height of the TOC */
.toc-content {
  font-size: .8rem;
  line-height: 1.1;
  cursor: pointer;

/* Indent headings based on their level */
.toc-h3 {
  padding-left: 1em;

.toc-h4 {
  padding-left: 2em;

.toc-h5 {
  padding-left: 3em;

.toc-h5 {
  padding-left: 4em;
Styling the TOC

The Demo

The proof is in the pudding: see the table of contents at the top of this page or this article's source if you're reading it somewhere else. You can also peep the source code for the TOC used on my site.

From my table of contents to yours 😍

What's Next