How to Auto-generated Table of Contents with HTML Slots

Table of contents can greatly improve the user experience of many websites, for instance documentation sites or online encyclopedias like Wikipedia. A well-designed table of contents gives an overview of the page and helps users quickly navigate to the section they are interested in.

Traditionally, you can create table of contents either in HTML or with JavaScript, but the lately standardized HTML slots provide a middle way between the two. HTML Slot is a web standard that allows you to add placeholders to a web page and later fill it with content dynamically.

When to use the <slot> tag

You can place <slot> tags into the table of contents inside your HTML file, so the slots later can be filled with the relevant headings and subheadings. When the headings are changed the slots are auto-updated.

With this technique, you need to create the HTML source code of the table of contents manually. JavaScript only auto-generates the text content of the table of contents, based on the headings or subheadings on the page.

If you don’t want to table of contents to be present in the HTML you need to generate both the layout and the content with JavaScript.

1. Create the HTML

The HTML source code for the TOC (table of contents) will be inside a <template> tag. The code inside <template> doesn’t get rendered until it’s added to the document by JavaScript. Our TOC will have placeholders, held in <slot> tags, for all the headings and subheadings found in the document.

The name attribute of each <slot> will have the same value as the slot attribute in their corresponding headings and subheadings in the document.

Below, you can see a sample HTML <article> with some headings and subheadings. The <div> at the beginning is where we’ll insert the auto-filled TOC.

<div id='toc'></div>
<article>
<p>Velociraptor (meaning "swift seizer" in Latin) is a …</p>

<h2 slot='h-1'>Description</h2>
<p>Velociraptor was a mid-sized dromaeosaurid, with adults …</p>

	<h3 slot='sh-1-1'>Feathers</h3>
	<p>Fossils of dromaeosaurids more primitive than …</p >

<h2 slot='h-2'>History of discovery</h2>
<p>During an American Museum of Natural History expedition …</p>

<h2 slot='h-3'>Classification</h2>
<p>Velociraptor is a member of the group Eudromaeosauria, a derived sub-group of …</p>

<h2 slot='h-4'>Paleobiology</h2>
<p>The "Fighting Dinosaurs" specimen, found in 1971, preserves a …</p>

	<h3 slot='sh-4-1'>Scavenging behavior</h3>
	<p>In 2010, Hone and colleagues published a paper on …</p>

	<h3 slot='sh-4-2'>Metabolism</h3>
	<p>Velociraptor was warm-blooded to some degree, as it required a …</p>

	<h3 slot='sh-4-3'>Pathology</h3>
	<p>One Velociratoptor mongoliensis skull bears two parallel …</p>
</article>

As you can see, each heading is given a unique slot value.

And, here’s the HTML code of the TOC, inside a <template> tag.

<template>
<ul>
	<li>
		<slot name='h-1'></slot>
		<ul>
			<li><slot name='sh-1-1'></slot></li>
		</ul>
	</li>
	<li><slot name='h-2'></slot></li>
	<li><slot name='h-3'></slot></li>
	<li>
		<slot name='h-4'></slot>
		<ul>
			<li><slot name='sh-4-1'></slot></li>
			<li><slot name='sh-4-2'></slot></li>
			<li><slot name='sh-4-3'></slot></li>
		</ul>
	</li>
</ul>
<style>
	ul {
		list-style: none;
	}
/* … */
</style>
</template>

In the two code snippets above, notice the matching slot and name attributes inside the headings and the <slot> tags.

2. Number the headings

Before looking into the JavaScript code that will add the TOC from the <template> to the document, let’s add serial numbers for the headings, using CSS counters.

article {
	counter-reset: heading;
}
article h2::before {
	counter-increment: heading;
	content: '0'counter(heading)': ';
}

Ensure that the counter-reset rule belongs to the element that’s the immediate parent of all the titles carrying the slot attribute (which is the <article> element in our code).

3. Insert the TOC into the document

Now, we add the script that inserts the TOC above the <article> tag, inside the <div id='toc'></div> container.

templateContent = document.querySelector('template').content;
article = document.querySelector('article').cloneNode(true);
article.attachShadow({  mode: 'closed' }).appendChild(templateContent.cloneNode(true));
document.querySelector('#toc').appendChild(article);

The code snippet above creates a copy of <article> and attaches a Shadow DOM Tree to it. We also add a copy of <template>‘s content to this Shadow DOM tree.

Then, the cloned <article> is inserted into the <div id='toc'> element. The <article> element is now present in the TOC as well, however only its headings and subheadings that found a placeholder inside the TOC are visible.

If we would reset the CSS counter at the body or html element instead of article, the counter would have counted the list of headings inside the TOC as well. That’s why you should reset the counters at the immediate parent of the headings.

Here is the screenshot of the output:

Auto-generated table of contents

4. Add hyperlinks

If you want to link the TOC titles to their respective headings and subheadings by adding id to the headings and anchoring their corresponding TOC text you will have to remove the repetitive id values from the cloned article.

<div id='toc'></div>
<article>
<p>Velociraptor (meaning "swift seizer" in Latin) is a …</p>

<h2 id='h-1' slot='h-1'>Description</h2>
<p>Velociraptor was a mid-sized dromaeosaurid, with adults …</p>

<h3 id='sh-1-1' slot='sh-1-1'>Feathers</h3>
<p>Fossils of dromaeosaurids more primitive than …</p >
<!-- ... -->
</article>

As you can see above, the id attribute is added to every heading and subheading in the article.

And, the titles inside the table of contents are anchored:

<template>
<ul>
<li>
		<a href='#h-1'><slot name='h-1'></slot></a>
		<ul>
			<a href='#sh-1-1'><li><slot name='sh-1-1'></slot></li></a>
		</ul>
</li>
<!-- ... -->
</ul>
</template>

In the extra line above, all id attributes are removed from the cloned article before attaching the Shadow DOM tree to it.

templateContent = document.querySelector('template').content;
article = document.querySelector('article').cloneNode(true);
article.querySelectorAll('*[id]').forEach((ele)=>{ele.removeAttribute('id')})
article.attachShadow({  mode: 'closed' }).appendChild(templateContent.cloneNode(true));
document.querySelector('#toc').appendChild(article);

See the screenshot of the linked table of contents below:

Linked table of contents

Github demo

You can check out, download, or fork the code used in this post from our Github Repo.

WebsiteFacebookTwitterInstagramPinterestLinkedInGoogle+YoutubeRedditDribbbleBehanceGithubCodePenWhatsappEmail