Créer des barres de progression animées au scroll en React
Pour créer des barres de progression animées au scroll en React, utilise useInView de Framer Motion pour détecter l'entrée dans le viewport, puis pilote le remplissage de chaque barre avec un useSpring connecté à un useMotionValue. Le même spring alimente un compteur de pourcentage en temps réel via useTransform.
- Stack : React 18 + Framer Motion 11 + propriétés CSS custom, ~240 lignes, zéro dépendance supplémentaire.
- API clé : useMotionValue, useSpring (damping 20, stiffness 60), useTransform, useInView.
- Chaque barre s'anime en séquence avec un décalage de 100ms (délai index * 100ms) après l'entrée en viewport.
- Accessible : les valeurs sont du texte lisible ; le remplissage de la barre est décoratif, pas d'ARIA nécessaire.
- Responsive : le layout split deux colonnes se replie correctement sur écrans étroits via CSS grid.
Metrics Animated Bars est une section React en layout split qui transforme des données en pourcentage en quelque chose qu'on observe se construire. Quand la section entre dans le viewport, les barres horizontales se remplissent de gauche à droite avec une animation en spring pendant qu'un compteur monte jusqu'à la valeur finale. Le layout pose le titre à gauche et la liste de barres à droite, laissant à chaque côté de l'espace.
Anatomie
La section est une grille CSS à deux colonnes égales. La colonne gauche contient un badge optionnel, un titre h2 et un sous-titre, qui apparaissent ensemble au scroll. La colonne droite contient la liste de barres : chaque élément est un sous-composant MetricBar avec une rangée label (texte, description optionnelle, valeur animée) au-dessus d'un track de 6px de haut renfermant la couche de remplissage animée.
Comment ça marche
Un seul hook useInView surveille le conteneur de la colonne droite avec une marge de -80px, déclenchement unique. Quand inView passe à true, chaque MetricBar lance un setTimeout décalé de index * 100ms, puis appelle motionVal.set(metric.value). Ce motionVal alimente un useSpring (damping 20, stiffness 60) qui s'approche de la valeur cible avec un léger rebond élastique. useTransform mappe la sortie du spring sur scaleX entre 0 et 1, ce qui pilote la mise à l'échelle du div de remplissage via transformOrigin:'left'. Un second useTransform convertit le même spring en chaîne de pourcentage pour le compteur, abonné via displayVal.on('change') pour rester synchronisé avec la barre.
Comment le coder en React
Initialise le motion value et le spring
Dans MetricBar, crée un useMotionValue à 0 et fais-le passer dans useSpring. Le damping et le stiffness du spring contrôlent l'élasticité du remplissage. Un stiffness plus élevé donne une barre plus vive ; un damping plus faible prolonge le rebond.
const motionVal = useMotionValue(0); const spring = useSpring(motionVal, { damping: 20, stiffness: 60 });Pilote le remplissage et le compteur depuis le même spring
Utilise deux appels useTransform sur le spring : un qui mappe [0, 100] vers [0, 1] pour scaleX du div de remplissage, et un autre qui formate la valeur en chaîne de pourcentage pour le compteur. Abonne-toi à la transformation de chaîne avec .on('change') pour maintenir un état React synchronisé.
const scaleX = useTransform(spring, [0, 100], [0, 1]); const displayVal = useTransform(spring, (v) => `${Math.round(v)}%`); useEffect(() => { const unsub = displayVal.on("change", (v) => setDisplayText(v)); return unsub; }, [displayVal]);Déclenche au scroll avec un décalage par barre
Pose une ref sur le div conteneur et passe-la à useInView avec once:true et une marge de -80px pour que l'animation se déclenche avant que la section soit entièrement visible. Quand inView passe à true, utilise un setTimeout avec index * 100ms avant de définir la motion value, pour que chaque barre se remplisse en séquence plutôt que simultanément.
const containerRef = useRef<HTMLDivElement>(null); const inView = useInView(containerRef, { once: true, margin: "-80px" }); useEffect(() => { if (inView) { const timer = setTimeout(() => { motionVal.set(metric.value); }, index * 100); return () => clearTimeout(timer); } }, [inView, metric.value, motionVal, index]);Applique scaleX au remplissage avec transformOrigin à gauche
Rends une motion.div dans le track en position:absolute et inset:0, mets transformOrigin à 'left', et lie la motion value scaleX directement à la prop style. La barre grandit alors de gauche à droite au fur et à mesure que la valeur du spring monte.
<motion.div style={{ position: "absolute", inset: 0, backgroundColor: "var(--color-accent)", transformOrigin: "left", scaleX, }} />
Quand l'utiliser
Utilise ce composant sur les pages À propos, Services ou Études de cas où tu veux présenter des données d'expertise ou de performance de façon lisible et visuelle. Il fonctionne mieux avec 4 à 8 métriques qui se prêtent naturellement à une représentation en pourcentage : niveaux de compétence, scores de satisfaction client, taux d'atteinte d'objectifs. À éviter quand les données ne sont pas naturellement des pourcentages, quand les métriques nécessitent des valeurs décimales précises, ou quand l'utilisateur doit comparer plusieurs jeux de données côte à côte, une bibliothèque de graphiques gère mieux ces cas.
Utilisé par
- Stripe, Utilise des indicateurs de progression animés dans l'onboarding de son dashboard pour afficher les taux de complétion.
- Webflow, Emploie des barres animées horizontales sur ses pages de tarifs et comparaisons de fonctionnalités pour mettre en avant les capacités des plans.
- Figma, Affiche des barres de remplissage animées dans ses sections de statistiques communautaires et d'utilisation des plugins.
FAQ
Pourquoi useSpring plutôt qu'un simple appel animate() ?
useSpring donne au remplissage une inertie physique : la barre dépasse légèrement puis se stabilise, ce qui paraît naturel plutôt que mécanique. Tu règles damping et stiffness pour correspondre à l'énergie de la marque sans toucher à une durée ou une courbe d'easing.
Peut-on afficher des valeurs autres que des pourcentages ?
Le composant attend des valeurs entre 0 et 100 pour le remplissage de la barre. Si tes données utilisent une autre échelle, normalise-les avant de les passer en props, et adapte le useTransform de displayVal pour formater le libellé comme tu le souhaites (monnaie, score, etc.).
Comment empêcher l'animation de se rejouer au re-render ?
Le hook useInView est appelé avec once:true, donc il se déclenche une seule fois lors de la première entrée du conteneur dans le viewport. Les re-renders ultérieurs du parent ne le réinitialisent pas. Si tu as besoin de rejouer l'animation, démonte et remonte le composant.
Le décalage pose-t-il problème avec beaucoup de barres ?
Chaque barre ajoute 100ms de délai, donc 8 barres font démarrer la dernière après 700ms. Ça reste perceptible et agréable. Au-delà de 10 barres le délai cumulé devient trop long ; envisage de réduire le décalage à 50ms ou de le plafonner à une valeur maximale.