Créer une timeline React dont le tracé SVG se dessine au scroll
Une timeline React dont le tracé se dessine au scroll relie la motion value pathLength de Framer Motion à la progression useScroll via un lissage useSpring. Le chemin se trace au fur et à mesure que la section entre dans le viewport, et chaque jalon grossit exactement quand le trait animé atteint sa position sur le chemin.
- Stack : React + Framer Motion 11 + tokens CSS inline, ~105 lignes, zéro dépendance supplémentaire.
- API clé : useScroll, useSpring, useTransform, motion.path pathLength, useInView.
- Le chemin SVG est calculé au rendu via un générateur de courbes de Bézier cubiques (buildPath) qui alterne des décalages gauche/droite pour produire la forme sinueuse.
- Accessible : le SVG est aria-hidden, le contenu textuel vit dans des éléments DOM standard.
- Les blocs de contenu alternent gauche/droite avec un reveal décalé sur l'axe x ; fonctionne sur mobile car le scroll est disponible sur tous les appareils.
Timeline Draw Path est une section React de timeline verticale où un tracé SVG sinueux se dessine de haut en bas pendant que l'utilisateur fait défiler la page. Les jalons s'allument quand la ligne animée les atteint, et chaque bloc de contenu glisse depuis son côté alternant. Le résultat donne l'impression que l'histoire s'écrit en temps réel, ce qui en fait un excellent choix pour les roadmaps produit, les historiques d'entreprise, ou les séquences de lancement de fonctionnalités.
Anatomie
Un bloc d'en-tête centré (eyebrow, h2, sous-titre) précède le corps de la timeline. Ce corps est un conteneur relatif qui contient simultanément un rail SVG positionné en absolu au centre horizontal, et une colonne de rangées de contenu avec du padding. Chaque rangée est un conteneur flex dont la hauteur correspond à la constante STEP_H fixe (180px), la carte de texte étant poussée à gauche ou à droite selon l'indice de l'étape. Le SVG contient un chemin guide gris statique, le motion.path coloré animé par-dessus, et le groupe MilestoneDot pour chaque étape.
Comment ça marche
useScroll observe l'élément section depuis que son sommet atteint 75% du viewport jusqu'à ce que son bas soit à 30%. La progression brute est lissée par useSpring (stiffness 80, damping 22) pour éliminer les saccades et ajouter de l'inertie. useTransform mappe la valeur du spring [0, 1] directement sur la motion value pathLength de Framer Motion. Côté SVG, un motion.path reçoit cette valeur dans son style pathLength et Framer Motion gère le calcul stroke-dashoffset. Chaque MilestoneDot mappe sa position (idx / total) dans une fenêtre [t-0.04, t+0.06] sur le même spring pour passer de 0 à 1, de sorte que les jalons apparaissent exactement quand le trait les atteint. Les blocs texte utilisent useInView avec once:true pour un reveal directionnel en une seule passe.
Comment le coder en React
Générer le chemin SVG sinueux
Écris une fonction buildPath qui part du milieu du SVG et ajoute une courbe de Bézier cubique par étape. Alterne le décalage horizontal du point de contrôle (+8 puis -8) entre les étapes paires et impaires pour créer l'onde. Termine par une ligne verticale jusqu'au bas de la dernière étape.
const PX = 40; // SVG midpoint x const STEP_H = 180; function buildPath(n: number): string { if (n === 0) return ""; let d = `M ${PX} 0`; for (let i = 0; i < n; i++) { const cy = i * STEP_H + STEP_H * 0.1; const py = i > 0 ? (i - 1) * STEP_H + STEP_H * 0.1 : 0; const mid = (py + cy) / 2; const off = i % 2 === 0 ? 8 : -8; d += ` C ${PX + off} ${mid}, ${PX - off} ${mid}, ${PX} ${cy}`; } return d + ` L ${PX} ${n * STEP_H}`; }Lier la progression du scroll à pathLength
Attache une ref au conteneur de la timeline et passe-la à useScroll avec offset ['start 0.75', 'end 0.3']. Lisse la progression brute avec useSpring, puis mappe-la sur [0, 1] via useTransform. Passe le résultat comme prop de style pathLength sur un motion.path.
const sectionRef = useRef<HTMLDivElement>(null); const { scrollYProgress } = useScroll({ target: sectionRef, offset: ["start 0.75", "end 0.3"], }); const smooth = useSpring(scrollYProgress, { stiffness: 80, damping: 22 }); const pathLength = useTransform(smooth, [0, 1], [0, 1]); // In JSX: <motion.path d={svgPath} stroke="var(--color-accent)" style={{ pathLength }} />Animer les jalons depuis le même spring
Pour chaque jalon à l'indice idx sur total étapes, calcule la position normalisée t = idx / total. Utilise useTransform sur le spring partagé avec une petite fenêtre autour de t pour faire varier scale et opacity de 0 à 1. Le jalon apparaît exactement quand le trait le rejoint.
function MilestoneDot({ progress, idx, total }) { const t = idx / total; const dotScale = useTransform(progress, [t - 0.04, t + 0.06], [0, 1]); const dotOpacity = useTransform(progress, [t - 0.04, t + 0.06], [0, 1]); // render motion.circle with style={{ scale: dotScale, opacity: dotOpacity }} }Révéler les blocs de contenu en direction alternante
Enveloppe chaque carte texte dans une motion.div avec initial={{ opacity: 0, x: isLeft ? 32 : -32 }}. Utilise useInView avec once:true sur la ref de la rangée et passe à animate={{ opacity: 1, x: 0 }} quand elle entre dans le viewport. Cela reflète l'alternance gauche/droite de l'onde du chemin.
const ref = useRef<HTMLDivElement>(null); const isInView = useInView(ref, { once: true, margin: "-12% 0px" }); const isLeft = index % 2 === 0; <motion.div initial={{ opacity: 0, x: isLeft ? 32 : -32, y: 6 }} animate={isInView ? { opacity: 1, x: 0, y: 0 } : {}} transition={{ duration: 0.65, ease: [0.16, 1, 0.3, 1] }} />
Quand l'utiliser
Utilise ce composant quand tu as une narration linéaire avec 4 à 6 jalons clairement datés : historique d'entreprise, roadmap produit, flux d'onboarding ou changelog de versions. La métaphore du tracé signal la progression de façon intuitive. Évite-le pour des grilles de fonctionnalités non ordonnées, des données profondément imbriquées, ou des pages où chaque utilisateur atterrit au milieu du scroll (l'animation requiert une entrée par le haut pour être lisible). Évite-le aussi quand le contenu a moins de 3 étapes ; l'onde n'a pas assez de place.
Utilisé par
- Stripe, Utilise des révélations de chemin progressives au scroll sur ses pages de storytelling produit pour guider les lecteurs à travers les jalons d'infrastructure.
- Linear, Emploie des animations SVG liées au scroll dans ses sections changelog et roadmap pour communiquer la dynamique.
- Framer, Met en avant des animations de tracé sur son propre site marketing comme démonstration directe de ses capacités d'animation.
- Vercel, Utilise des connecteurs pilotés au scroll entre les blocs de fonctionnalités sur ses pages produit d'infrastructure pour raconter une histoire de déploiement.
FAQ
Pourquoi le chemin est-il une onde de Bézier cubique plutôt qu'une ligne droite ?
La courbe alternante donne à la ligne un mouvement organique et sépare visuellement les blocs de contenu gauche et droite sans nécessiter une grille en deux colonnes. Une ligne verticale droite fonctionne aussi, mais elle se lit comme un simple séparateur plutôt que comme un fil narratif.
Puis-je changer le nombre d'étapes sans toucher au SVG ?
Oui. La fonction buildPath prend le nombre d'étapes en argument et la hauteur du SVG est dérivée de steps.length * STEP_H. Passe plus ou moins d'étapes dans la prop steps et le rail se recalcule automatiquement.
L'animation démarre à moitié quand les utilisateurs arrivent sur un lien d'ancre profond dans la page. Comment corriger ça ?
useScroll calcule la progression depuis la position de scroll actuelle au montage. Si la page se charge déjà scrollée, le chemin apparaîtra partiellement dessiné. Une solution consiste à remettre scrollYProgress à 0 dans un useEffect au montage, ou à conditionner la section à un point d'entrée qui ramène en haut de page.
Comment adapter la mise en page pour une vue mobile verticale où l'alternance gauche/droite s'effondre ?
Sur les écrans étroits, remplace la justification flex pour toujours utiliser flex-start et supprime le décalage x horizontal afin que toutes les cartes s'empilent d'un seul côté. Le rail SVG reste centré ; seul l'alignement de la colonne de contenu change. Une propriété CSS personnalisée ou une vérification de la largeur de fenêtre dans le composant suffit.