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.
Read Also: How to Use HTML <template> & <slot> With Shadow DOM
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:
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:
Github demo
You can check out, download, or fork the code used in this post from our Github Repo.
Read Also: How to Create a Text-Search Bookmarklet with JavaScript