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>
The Advanced Solution
The solution I provide to create a table of contents is simple. It gets the job done.
![](https://ryanfeigenbaum.com/content/images/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 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');
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
}
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>';
details.append(summary);
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:
- We extract the text from the heading and add it to the table of contents in a
p
tag - We assign the table-of-content element a class based on the heading level (which will allow us to apply semantic styling like indents)
- We add a link to the table-of-content element to take the reader to that section of the article
- 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 = '#' + el.id;
p.append(a);
details.append(p);
});
// Add the table of contents to the beginning of the article
articleContainer.prepend(details);
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.id = el.id || el.textContent.toLocaleLowerCase().replace(/\W/g,"-");
// Rest of the loop
}
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.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);
}
The resulting TOC markup will look like the following:
<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>
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;
}
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 😍