Inhaltsverzeichnis aus Überschriften in JavaScript erstellen
Lange Seiten ohne Navigation frustrieren Leser. Sie scrollen ziellos, verlieren den Überblick und springen ab. Ein dynamisches Inhaltsverzeichnis löst dieses Problem, indem es Ihre vorhandene Überschriftenstruktur automatisch in klickbare Anker-Links umwandelt – ganz ohne Bibliothek oder Framework.
Dieser Artikel zeigt Ihnen, wie Sie eines mit modernem Vanilla-JavaScript erstellen, gängige Edge Cases behandeln, eine barrierefreie TOC-Generierung umsetzen und optional den aktiven Abschnitt beim Scrollen hervorheben.
Die wichtigsten Erkenntnisse
- Verwenden Sie einen eingegrenzten Selektor für Überschriften, um unverwandte Überschriften aus Headern, Seitenleisten oder Footern auszuschließen.
- Erzeugen Sie kollisionssichere, URL-freundliche IDs, indem Sie den Überschriftentext slugifizieren und Duplikate mit einem Zähler verfolgen.
- Umschließen Sie die generierte Liste mit einem
<nav>-Element samtaria-label, um einen barrierefreien Navigations-Landmark zu schaffen. - Verwenden Sie
IntersectionObserveranstelle von Scroll-Listenern, um den aktiven Abschnitt effizient hervorzuheben. - Aktivieren Sie sanftes Scrollen mit einer einzigen CSS-Regel:
scroll-behavior: smooth.
Wie DOM-Überschriftennavigation funktioniert
Die Grundidee ist unkompliziert:
- Alle Überschriftenelemente aus dem DOM abfragen.
- Eine sichere
idfür jede Überschrift erzeugen. - Eine Liste von Anker-Links erstellen, die auf diese IDs verweisen.
- Die Liste in einen vorgesehenen Container einfügen.
Kein Regex-Parsing von rohem HTML. Kein jQuery. Nur querySelectorAll und standardmäßige DOM-Methoden.
Überschriften auswählen und vorbereiten
const headings = document.querySelectorAll('article h2, article h3, article h4');
Beschränken Sie Ihren Selektor auf den Inhaltsbereich – article, main oder einen bestimmten Container – damit Sie nicht versehentlich Überschriften aus dem Seiten-Header oder der Seitenleiste übernehmen.
Sichere Anker-IDs generieren
Überschriftentexte enthalten oft Leerzeichen, Sonderzeichen oder Satzzeichen, die URL-Fragmente schwerer lesbar oder umständlich in Selektoren machen. Normalisieren Sie die id jeder Überschrift vor der Verwendung:
function slugify(text) {
return text
.toLowerCase()
.trim()
.replace(/\s+/g, '-')
.replace(/[^\w-]/g, '') || 'section';
}
Behandeln Sie Duplikate, indem Sie verwendete Slugs verfolgen und einen Zähler anhängen:
const usedIds = new Map();
function uniqueSlug(text) {
const base = slugify(text);
const count = usedIds.get(base) ?? 0;
usedIds.set(base, count + 1);
return count === 0 ? base : `${base}-${count}`;
}
Überspringen Sie leere oder nur aus Whitespace bestehende Überschriften mit einer einfachen Prüfung: if (!heading.textContent.trim()) return;.
Discover how at OpenReplay.com.
Das JavaScript-Inhaltsverzeichnis aufbauen
Nachdem die IDs zugewiesen sind, bauen Sie die TOC-Liste auf und fügen sie in die Seite ein:
function buildTOC(containerSelector, headingSelector) {
const container = document.querySelector(containerSelector);
const headings = document.querySelectorAll(headingSelector);
if (!container || !headings.length) return;
const nav = document.createElement('nav');
nav.setAttribute('aria-label', 'Table of contents');
const ol = document.createElement('ol');
headings.forEach(heading => {
const text = heading.textContent.trim();
if (!text) return;
const id = heading.id || uniqueSlug(text);
heading.id = id;
const li = document.createElement('li');
const a = document.createElement('a');
a.href = `#${id}`;
a.textContent = text;
li.appendChild(a);
ol.appendChild(li);
});
nav.appendChild(ol);
container.appendChild(nav);
}
document.addEventListener('DOMContentLoaded', () => {
buildTOC('#toc', 'article h2, article h3');
});
Die Verwendung von DOMContentLoaded stellt sicher, dass die Überschriften vorhanden sind, bevor das Skript ausgeführt wird.
Barrierefreie TOC-Generierung
Das Umschließen der Liste mit einem <nav>-Element samt aria-label="Table of contents" erzeugt einen benannten Navigations-Landmark. Screenreader machen diese Landmarks direkt zugänglich, sodass Nutzer zum TOC springen können, ohne sich durch die Seite zu tabben.
Bewahren Sie eine logische Überschriftenhierarchie in Ihren Inhalten – springen Sie nicht von h2 direkt zu h4. Das TOC spiegelt die Struktur Ihres Dokuments wider, sodass Lücken in den Überschriftenebenen für alle Nutzer zu verwirrender Navigation führen.
Den aktiven Abschnitt mit IntersectionObserver hervorheben
Um hervorzuheben, welcher Abschnitt aktuell sichtbar ist, verwenden Sie die IntersectionObserver-API anstelle von Scroll-Event-Listenern:
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
const id = entry.target.getAttribute('id');
const link = document.querySelector(`nav a[href="#${id}"]`);
if (link) link.classList.toggle('active', entry.isIntersecting);
});
}, { rootMargin: '0px 0px -80% 0px' });
document.querySelectorAll('article h2, article h3').forEach(h => observer.observe(h));
Das rootMargin verkleinert die Erkennungszone so, dass eine Überschrift nur dann als aktiv markiert wird, wenn sie sich nahe am oberen Rand des Viewports befindet. Dieser Ansatz ist performanter als gedrosselte Scroll-Handler und erfordert für die meisten statischen Seiten keinerlei Bereinigung. IntersectionObserver wird von allen modernen Browsern unterstützt.
Sanftes Scrollen
Fügen Sie diese einzige CSS-Regel hinzu, um seitenweit sanftes Scroll-Verhalten ohne JavaScript zu aktivieren:
html {
scroll-behavior: smooth;
}
Die Eigenschaft scroll-behavior wird von allen modernen Evergreen-Browsern unterstützt.
Fazit
Ein funktionierendes JavaScript-Inhaltsverzeichnis benötigt vier Dinge: eingegrenzte Überschriftenauswahl, kollisionssichere ID-Generierung, einen semantischen <nav>-Wrapper für barrierefreie TOC-Generierung und korrektes Timing mit DOMContentLoaded. Die Erweiterung mit IntersectionObserver ist optional, bringt aber mit minimalem Code eine spürbare UX-Verbesserung.
Dieses Muster funktioniert auf jeder statischen Website, Dokumentationsseite oder jedem Blog – ohne Build-Schritt, ohne Abhängigkeiten, ohne Framework.
FAQs
Beides funktioniert, aber die clientseitige Generierung ist einfacher und hält die Logik an einer Stelle. Serverseitiges Rendering ist für SEO-kritische Seiten oder bei deaktiviertem JavaScript vorzuziehen, da die Anker-Links und Überschriften-IDs dann bereits im initialen HTML enthalten sind. Für die meisten Blogs und Dokumentationsseiten reicht die clientseitige Generierung mit DOMContentLoaded aus.
Prüfen Sie vor der Generierung einer neuen ID, ob bereits eine vorhanden ist. Wenn eine Überschrift eine manuell zugewiesene id hat, verwenden Sie diese weiter, damit externe Links und Lesezeichen funktionieren. Fügen Sie eine Bedingung wie if heading.id ein, um den vorhandenen Wert zu verwenden, andernfalls rufen Sie uniqueSlug auf. So bleibt die Absicht des Autors gewahrt und eingehende Links auf bestimmte Abschnitte werden nicht gebrochen.
Scroll-Events feuern Dutzende Male pro Sekunde und erzwingen bei jedem Aufruf Layout-Berechnungen, was vor allem auf Mobilgeräten die Performance beeinträchtigt. IntersectionObserver beobachtet Sichtbarkeitsänderungen asynchron und feuert nur dann, wenn sich die Sichtbarkeit tatsächlich ändert. Außerdem lassen sich Schwellenwerte und Margins deklarativ konfigurieren, ohne dass Sie eigene Throttle- oder Debounce-Logik schreiben müssen.
Verfolgen Sie die aktuelle Überschriftenebene während der Iteration. Wenn die nächste Überschrift tiefer liegt, erstellen Sie ein verschachteltes ol innerhalb des vorherigen li. Wenn sie höher liegt, gehen Sie die Elternkette wieder hinauf. Das Speichern eines Stacks von Listenelementen, indiziert nach Überschriftenebene, hält die Logik sauber und unterstützt beliebige Verschachtelungstiefen, ohne dass jede Ebene fest verdrahtet werden muss.
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.