Créer une table des matières à partir des titres en JavaScript
Les pages longues sans navigation frustrent les lecteurs. Ils défilent sans but, perdent leur repère et finissent par partir. Une table des matières dynamique résout ce problème en transformant la structure de vos titres existante en liens d’ancrage cliquables — automatiquement, sans bibliothèque ni framework.
Cet article vous montre comment en construire une en utilisant du JavaScript vanilla moderne, gérer les cas particuliers courants, ajouter une génération de table des matières accessible et, en option, mettre en évidence la section active à mesure que l’utilisateur fait défiler.
Points clés à retenir
- Interrogez les titres avec un sélecteur restreint pour éviter de récupérer des titres non pertinents provenant des en-têtes, barres latérales ou pieds de page.
- Générez des
idcompatibles URL et résistants aux collisions en sluggifiant le texte des titres et en suivant les doublons à l’aide d’un compteur. - Encapsulez la liste générée dans un élément
<nav>avec unaria-labelpour créer un repère de navigation accessible. - Utilisez
IntersectionObserverplutôt que des écouteurs de défilement pour mettre en évidence efficacement la section active. - Activez le défilement fluide grâce à une simple règle CSS :
scroll-behavior: smooth.
Comment fonctionne la navigation par titres dans le DOM
L’idée fondamentale est simple :
- Interroger tous les éléments de titre du DOM.
- Générer un
idsûr pour chaque titre. - Construire une liste de liens d’ancrage pointant vers ces ID.
- Injecter la liste dans un conteneur désigné.
Pas d’analyse regex de HTML brut. Pas de jQuery. Juste querySelectorAll et les méthodes standards du DOM.
Sélectionner et préparer les titres
const headings = document.querySelectorAll('article h2, article h3, article h4');
Restreignez votre sélecteur à la zone de contenu — article, main ou un conteneur spécifique — afin de ne pas récupérer accidentellement les titres de l’en-tête de votre site ou de la barre latérale.
Générer des ID d’ancrage sûrs
Le texte des titres contient souvent des espaces, caractères spéciaux ou signes de ponctuation qui rendent les fragments d’URL moins lisibles ou peu pratiques à utiliser dans les sélecteurs. Normalisez l’id de chaque titre avant de l’utiliser :
function slugify(text) {
return text
.toLowerCase()
.trim()
.replace(/\s+/g, '-')
.replace(/[^\w-]/g, '') || 'section';
}
Gérez les doublons en suivant les slugs utilisés et en ajoutant un compteur :
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}`;
}
Ignorez les titres vides ou ne contenant que des espaces avec une simple condition de garde : if (!heading.textContent.trim()) return;.
Discover how at OpenReplay.com.
Construire la table des matières en JavaScript
Une fois les ID attribués, construisez la liste de la table des matières et injectez-la dans la page :
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');
});
L’utilisation de DOMContentLoaded garantit que les titres existent avant l’exécution du script.
Génération accessible de la table des matières
Encapsuler la liste dans un élément <nav> avec aria-label="Table of contents" crée un repère de navigation nommé. Les lecteurs d’écran exposent ces repères directement, permettant aux utilisateurs d’accéder à la table des matières sans parcourir la page avec la touche Tab.
Préservez une hiérarchie logique des titres dans votre contenu — ne sautez pas d’un h2 à un h4. La table des matières reflète la structure de votre document : les sauts dans les niveaux de titre produisent une navigation déroutante pour tous les utilisateurs.
Mettre en évidence la section active avec IntersectionObserver
Pour mettre en évidence la section actuellement visible, utilisez l’API IntersectionObserver plutôt que des écouteurs d’événements de défilement :
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));
Le rootMargin réduit la zone de détection afin qu’un titre ne soit marqué comme actif que lorsqu’il est proche du haut de la fenêtre d’affichage. Cette approche est plus performante que des gestionnaires de défilement avec throttling et ne nécessite aucun nettoyage pour la plupart des pages statiques. IntersectionObserver est pris en charge par tous les navigateurs modernes.
Défilement fluide
Ajoutez cette unique règle CSS pour activer le comportement de défilement fluide sur l’ensemble de la page, sans aucun JavaScript :
html {
scroll-behavior: smooth;
}
La propriété scroll-behavior est prise en charge par tous les navigateurs evergreen modernes.
Conclusion
Une table des matières JavaScript fonctionnelle nécessite quatre éléments : une sélection ciblée des titres, une génération d’ID résistante aux collisions, un wrapper sémantique <nav> pour une génération accessible, et un timing basé sur DOMContentLoaded. L’amélioration via IntersectionObserver est facultative, mais apporte une UX significative avec un minimum de code.
Ce schéma fonctionne dans n’importe quel site statique, page de documentation ou blog — sans étape de build, sans dépendances, sans framework requis.
FAQ
Les deux fonctionnent, mais la génération côté client est plus simple et regroupe la logique en un seul endroit. Le rendu côté serveur est préférable pour les pages critiques pour le SEO ou lorsque JavaScript est désactivé, car les liens d'ancrage et les ID de titre seront présents dans le HTML initial. Pour la plupart des blogs et sites de documentation, la génération côté client avec DOMContentLoaded est suffisante.
Vérifiez l'existence d'un id avant d'en générer un nouveau. Si un titre a un id attribué manuellement, réutilisez-le pour que les liens externes et les signets continuent de fonctionner. Ajoutez une condition du type : si heading.id est défini, utilisez la valeur existante, sinon appelez uniqueSlug. Cela préserve l'intention de l'auteur et évite de casser les liens entrants vers des sections spécifiques.
Les événements de défilement se déclenchent des dizaines de fois par seconde et forcent des calculs de mise en page à chaque appel, ce qui nuit aux performances, en particulier sur mobile. IntersectionObserver observe les changements de visibilité de manière asynchrone et ne se déclenche que lorsque la visibilité change réellement. Il est également plus facile de configurer des seuils et des marges de manière déclarative, sans avoir à écrire soi-même une logique de throttle ou de debounce.
Suivez le niveau de titre courant à mesure que vous itérez. Lorsque le titre suivant est plus profond, créez un ol imbriqué à l'intérieur du li précédent. Lorsqu'il est moins profond, remontez la chaîne parente. Stocker une pile d'éléments de liste indexée par niveau de titre garde la logique propre et prend en charge une profondeur d'imbrication arbitraire sans coder en dur chaque niveau.
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.