Retour au catalogue

Blog Interactive TOC

Article de blog avec table des matieres interactive en sidebar et surlignage au scroll.

blogmedium Both Responsive a11y
minimaleditorialuniversalsaaseducationsplit
Theme

Créer un sommaire sticky avec suivi au scroll en React

Un sommaire interactif sticky en React utilise une nav latérale qui reste fixe pendant que le contenu défile. Chaque section s'enregistre via le callback onViewportEnter de Framer Motion, qui met à jour l'ID actif dans le state local et change le surlignage du lien correspondant dans le sommaire.

  • Stack : React 19 + Framer Motion 11 + Lucide React, ~120 lignes, zéro dépendance supplémentaire.
  • La détection au scroll utilise onViewportEnter de Framer Motion avec une marge de -80px, sans IntersectionObserver manuel.
  • Accessible : la nav porte aria-label="Table des matières" et les items du sommaire sont de vrais éléments <button>.
  • Caveat responsive : la sidebar sticky disparaît sur petits écrans ; prévoir un drawer mobile ou un sommaire en haut de page.
  • Thème via des custom properties CSS (--color-accent, --color-border), aucune couleur codée en dur.

Blog Interactive TOC est un layout d'article deux colonnes avec une sidebar gauche sticky qui suit la position du lecteur dans le contenu. À mesure que chaque section entre dans le viewport, son entrée dans le sommaire se surligne avec une bordure gauche accentuée, donnant au lecteur un repère visuel immédiat dans un long texte, le détail de navigation qui distingue une documentation soignée d'une simple page défilante.

Anatomie

Le composant possède trois zones visuelles. En haut, un header d'article pleine largeur affiche le tag de catégorie, le titre, l'auteur, la date et le temps de lecture avec des icônes Lucide. En dessous, une grille CSS divise la vue en une colonne nav sticky de 220px et une colonne de contenu fluide. La nav liste les titres de sections en boutons avec une bordure gauche commune ; l'item actif reçoit un surlignage de bordure gauche colorée. Chaque section de contenu est une motion.div avec son propre fade-in décalé et un hook onViewportEnter.

Comment ça marche

Le mécanisme de scroll-spy évite le boilerplate IntersectionObserver en déléguant au tracking de viewport de Framer Motion. Chaque section de contenu est encapsulée dans une motion.div avec onViewportEnter={() => setActiveId(s.id)} et une marge viewport de -80px. Quand plus de 80px d'une section entrent dans la zone visible, setActiveId se déclenche et la mise à jour du state re-rend le sommaire avec le nouveau surlignage actif. Un clic sur un bouton du sommaire appelle aussi setActiveId directement, gardant la navigation clavier en phase avec la position du scroll.

Comment le coder en React

  1. Définir la structure de données et initialiser le state

    Crée une interface Section avec id, title et content. Accepte un tableau sections en prop et initialise activeId avec useState, en prenant l'ID de la première section par défaut. Ce seul morceau de state pilote à la fois le surlignage du sommaire et la logique de scroll-spy.

    const [activeId, setActiveId] = useState(sections[0]?.id ?? "");
  2. Construire la nav sidebar sticky

    Rends une motion.nav avec position:sticky et top:2rem. Parcours les sections pour produire un bouton par entrée. Pilote la couleur de la bordure gauche et le font-weight depuis activeId, les items actifs reçoivent --color-accent et le poids 600, les inactifs --color-foreground-muted et le poids 400. Applique une transition CSS sur color et border-color pour un changement fluide.

    borderLeftColor: activeId === s.id ? "var(--color-accent)" : "transparent",
    fontWeight: activeId === s.id ? 600 : 400,
  3. Attacher onViewportEnter aux sections de contenu

    Encapsule chaque section d'article dans une motion.div et passe onViewportEnter={() => setActiveId(s.id)}. Règle la marge du viewport à -80px pour que le surlignage se déclenche légèrement avant que le haut de la section n'atteigne le bord supérieur de l'écran. Décale le fade-in initial avec delay: i * 0.05 pour un effet en cascade au premier chargement.

    <motion.div
      onViewportEnter={() => setActiveId(s.id)}
      viewport={{ once: true, margin: "-80px" }}
      initial={{ opacity: 0, y: 16 }}
      whileInView={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5, delay: i * 0.05, ease: EASE }}
    >
  4. Connecter la navigation au clic et gérer le layout responsive

    Le onClick de chaque bouton du sommaire appelle setActiveId directement pour que les utilisateurs clavier et souris restent synchronisés sans événements de scroll. Sur mobile, replie la sidebar dans un accordéon en haut de page ou un drawer, la colonne sticky de 220px est trop étroite pour les petits écrans et doit être masquée sous un breakpoint avec une media query CSS ou un préfixe responsive Tailwind.

Quand l'utiliser

Utilise ce layout pour les contenus longs où les lecteurs ont besoin de repères : documentation technique, tutoriels approfondis, études de cas, ou articles éditoriaux avec quatre sections nommées ou plus. À éviter pour les courts billets (moins de trois sections) où la sidebar ajoute du bruit visuel sans valeur de navigation, et pour les landing pages marketing où une sidebar CTA sticky surpasse un sommaire.

Utilisé par

  • Stripe Docs, Sommaire sticky côté droit avec surlignage de la section active sur toutes les pages de référence API et de guides.
  • MDN Web Docs, Sommaire sidebar persistant qui suit la position au scroll et marque le titre courant pour les longs articles techniques.
  • Vercel Docs, Layout deux colonnes avec un sommaire flottant qui surligne les sections au scroll dans tous ses guides de déploiement.

FAQ

Pourquoi utiliser onViewportEnter de Framer Motion plutôt qu'IntersectionObserver ?

onViewportEnter est déjà disponible sur toute motion.div ajoutée pour les animations, donc pas de configuration d'observer supplémentaire, pas de câblage de ref, et pas de nettoyage nécessaire. Si Framer Motion est déjà dans ton projet, c'est quatre caractères d'API supplémentaire.

Comment gérer le vrai défilement ancre en page plutôt que le simple surlignage visuel ?

Remplace le onClick du bouton par document.getElementById(s.id)?.scrollIntoView({ behavior: 'smooth' }) et garde setActiveId uniquement dans onViewportEnter. Ainsi le sommaire pilote le vrai défilement et le surlignage reste synchronisé via le callback de viewport.

La sidebar sticky fonctionne-t-elle avec les layouts App Router de Next.js ?

Oui, mais assure-toi que le conteneur de scroll est la fenêtre document et non une div imbriquée en overflow:auto. App Router encapsule les pages dans un layout racine, si ce layout a overflow:hidden ou overflow:auto, position:sticky perd sa référence et cesse de fonctionner.

Comment rendre le sommaire accessible aux utilisateurs clavier ?

L'implémentation actuelle utilise déjà des éléments button sémantiques, ils sont donc focusables et activables via Entrée ou Espace sans travail supplémentaire. Ajoute aria-current="true" sur l'item actif pour que les lecteurs d'écran annoncent quelle section est sélectionnée quand le focus se déplace dans la liste.

"use client";

import { useState } from "react";
import { motion } from "framer-motion";
import { Clock, User, Tag } from "lucide-react";

interface ArticleMeta {
  author: string;
  date: string;
  readTime: string;
  category: string;
}

interface Section {
  id: string;
  title: string;
  content: string;
}

interface BlogInteractiveTocProps {
  articleTitle?: string;
  articleMeta?: ArticleMeta;

Code complet réservé à Pro

Code source intégral, export multi-framework et playground.

Passer en Pro, 9,99€/mois

Avis

Sommaire React sticky avec scroll spy, Code + Tutoriel