Add a Simple Table of Contents to Your Website
Tags: javascript, webdev, css
Published: 2022-01-25 02:43:21

---
title: Add a Simple Table of Contents to Your Website
published: true
date: 2022-01-24 15:00:00 UTC
tags: JS,WebDev,CSS
canonical_url: https://ryanfeigenbaum.com/toc/
---

 ![Add a Simple Table of Contents to Your Website](https://ryanfeigenbaum.com/content/images/2022/01/toc.webp)

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:

```html
<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.

![Add a Simple Table of Contents to Your Website](https://media.tenor.com/images/86f2936e3b4e83969d4096dc3a2635b2/tenor.gif)

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](https://tscanlin.github.io/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.

```js
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.

```js
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.

```js
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>';
details.append(summary);
```

_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

```js
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 = '#' + el.id;
    p.append(a);
    details.append(p);
  });

// Add the table of contents to the beginning of the article  
articleContainer.prepend(details);
```

_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.

```js
headings.forEach((el) => {
	// Generate IDs if they don't exist
    el.id = el.id || 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

```js
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.id = el.id || 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>";
  details.append(summary);
  
  headings.forEach((el) => {
    el.id = el.id || 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 = "#" + el.id;
    p.append(a);
    details.append(p);
  });

  // Add the table of contents to the beginning of the article
  articleContainer.prepend(details);
}
```

_Complete TOC code_

The resulting TOC markup will look like the following:

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

_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.

```css
/* 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](#mel-main) or [this article's source](https://ryanfeigenbaum.com/toc#mel-main) if you're reading it somewhere else. You can also peep [the source code](https://github.com/royalfig/mel/blob/main/src/js/post/toc.js) for the TOC used on my site.

From my table of contents to yours 😍