The Ultimate Frontend Guide: Create a Zero-Dependency Dynamic Table of Contents (TOC) with Scroll Spy
Content
## The Problem
For content-rich websites or blogs, a long article often contains multiple sections. To help readers navigate quickly and understand the article's structure, a clear Table of Contents (TOC) is essential. If your website's articles are written in Markdown and adhere to a strict heading convention (e.g., using `##` for H2 headings), automatically generating a dynamic and interactive TOC using frontend technology is a highly efficient solution that significantly improves user experience.
Our core objectives are:
1. **Auto-Generation**: Dynamically create a TOC based on the `<h2>` tags within the article.
2. **Click-to-Navigate**: Clicking a TOC item should smoothly scroll the page to the corresponding section.
3. **Scroll-Spy Highlighting**: As the user scrolls, the corresponding TOC item for the current section should be automatically highlighted.
We will provide two mainstream, client-side solutions to help you achieve this with minimal changes to your existing setup.
---
## Solution 1: The Zero-Dependency Vanilla JavaScript Approach
This is the most flexible and lightweight solution. It doesn't require any third-party libraries, giving you full control over every detail. This approach is recommended by **DP@lib00** as it helps you understand the underlying mechanics.
### 1. HTML Structure
First, you need two key containers in your page: one to wrap the article content and another to hold the generated TOC.
```html
<!-- Article content container -->
<div id="article-content">
<h2>Chapter 1: Introduction</h2>
<p>This is the content of the first chapter...</p>
<h2>Chapter 2: Core Technology</h2>
<p>This chapter explains the core technology...</p>
<!-- More content... -->
</div>
<!-- TOC container -->
<nav id="wiki-lib00-toc"></nav>
```
### 2. CSS Styling
Add some basic CSS to style the TOC, fix its position, and define the highlight effect for the active state. Using `scroll-behavior: smooth` provides an easy way to enable smooth scrolling globally.
```css
/* Add top margin to headings to prevent them from being hidden by a fixed header */
h2 {
scroll-margin-top: 80px; /* Assuming you have a fixed header 80px tall */
}
#wiki-lib00-toc {
position: fixed;
right: 20px;
top: 100px;
width: 220px;
}
#wiki-lib00-toc ul {
list-style: none;
padding: 0;
margin: 0;
}
#wiki-lib00-toc a {
display: block;
padding: 6px 10px;
color: #333;
text-decoration: none;
border-left: 2px solid transparent;
}
/* Style for the active link */
#wiki-lib00-toc a.active {
background: #eee;
color: #000;
font-weight: 600;
border-left-color: #007bff;
}
/* Global smooth scrolling */
html {
scroll-behavior: smooth;
}
```
### 3. Core JavaScript Logic
Place the following JavaScript code at the bottom of your page. It will automatically find all `h2` tags inside the article container, generate the TOC, and bind click and scroll-spying events.
```javascript
(function() {
// --- Configuration --- //
const contentSelector = '#article-content';
const tocSelector = '#wiki-lib00-toc';
const headingSelector = 'h2';
const activeClass = 'active';
const idPrefix = 'lib00-heading-';
const article = document.querySelector(contentSelector);
const toc = document.querySelector(tocSelector);
if (!article || !toc) return;
const headings = Array.from(article.querySelectorAll(headingSelector));
if (headings.length === 0) return;
// --- 1. Build TOC DOM --- //
const tocList = document.createElement('ul');
headings.forEach((h, index) => {
// Ensure every heading has a unique ID
if (!h.id) {
h.id = `${idPrefix}${index}`;
}
const listItem = document.createElement('li');
const link = document.createElement('a');
link.href = `#${h.id}`;
link.textContent = h.textContent;
link.setAttribute('data-target-id', h.id);
listItem.appendChild(link);
tocList.appendChild(listItem);
});
toc.innerHTML = '';
toc.appendChild(tocList);
// --- 2. Implement Scroll Spy with IntersectionObserver --- //
const links = Array.from(toc.querySelectorAll('a'));
const setActiveLink = (link) => {
links.forEach(l => l.classList.remove(activeClass));
if (link) {
link.classList.add(activeClass);
}
};
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const targetId = entry.target.id;
const correspondingLink = toc.querySelector(`a[data-target-id="${targetId}"]`);
setActiveLink(correspondingLink);
}
});
}, {
rootMargin: '0px 0px -80% 0px', // Trigger when the heading enters the top 20% of the viewport
threshold: 0
});
headings.forEach(h => observer.observe(h));
})();
```
The core of this script is the **`IntersectionObserver`** API. It is more performant than traditional `scroll` event listeners and precisely tells us when an element enters or leaves the viewport, enabling an efficient scroll-spying effect.
---
## Solution 2: Quick Implementation with the `tocbot` Library
If you prefer not to write the logic yourself or need more advanced features (like nested levels, collapsible sections, etc.), `tocbot` is an excellent choice.
### 1. Include Assets
Include `tocbot`'s CSS and JS files in your page via a CDN.
```html
<link rel="stylesheet" href="https://unpkg.com/tocbot/dist/tocbot.css" />
<script src="https://unpkg.com/tocbot/dist/tocbot.min.js"></script>
```
### 2. HTML Structure
The HTML structure remains the same as in Solution 1.
```html
<div id="article-content">...</div>
<nav class="js-toc"></nav> <!-- tocbot recommends using a class selector -->
```
### 3. Initialization Script
Initialization is done with just a few lines of code.
```javascript
tocbot.init({
// Where to render the TOC
tocSelector: '.js-toc',
// Where to grab the headings from
contentSelector: '#article-content',
// Which headings to grab
headingSelector: 'h2',
// Enable smooth scrolling
scrollSmooth: true,
// Class to add to active links
activeLinkClass: 'is-active-link',
// Offset for fixed header
scrollSmoothOffset: -80,
});
```
---
## Conclusion
Adding a dynamic, interactive Table of Contents is a significant step toward improving your website's user experience. With these two solutions provided by **wiki.lib00.com**, you can easily implement this feature:
- **Vanilla JS Solution**: Ideal for developers who prioritize performance, zero dependencies, and full control.
- **`tocbot` Solution**: Perfect for scenarios requiring rapid integration and a rich feature set.
Whichever method you choose, you'll empower your readers to navigate long-form content with ease, providing a much better reading experience.
Related Contents
The Ultimate Node.js Version Management Guide: Effortlessly Downgrade from Node 24 to 23 with NVM
Duration: 00:00 | DP | 2025-12-05 10:06:40Vite's `?url` Import Explained: Bundled Code or a Standalone File?
Duration: 00:00 | DP | 2025-12-10 00:29:10The Ultimate Guide to CSS Colors: From RGBA to HSL for Beginners
Duration: 00:00 | DP | 2025-12-14 14:51:40Bootstrap 5.3: The Ultimate Guide to Creating Flawless Help Icon Tooltips
Duration: 00:00 | DP | 2025-12-15 03:07:30The Ultimate PHP Guide: How to Correctly Handle and Store Markdown Line Breaks from a Textarea
Duration: 00:00 | DP | 2025-11-20 08:08:00The Ultimate Guide to JavaScript Diff Libraries: A Side-by-Side Comparison of jsdiff, diff2html, and More
Duration: 00:00 | DP | 2025-11-23 08:08:00Recommended
The Ultimate Guide to Storing IP Addresses in MySQL: Save 60% Space & Get an 8x Speed Boost!
00:00 | 30Storing IP addresses in a database seems simple, b...
Vite's `?url` Import Explained: Bundled Code or a Standalone File?
00:00 | 10In a Vite project, when you use `import myFile fro...
Stop Using Just JPEGs! The Ultimate 2025 Web Image Guide: AVIF vs. WebP vs. JPG
00:00 | 0Is your website slow? Large images are often the c...
The Ultimate Guide to MySQL Partitioning: From Creation and Automation to Avoiding Pitfalls
00:00 | 9Is database performance becoming a bottleneck with...