Créer une section React à étapes pilotées par le scroll
Une section React à étapes pilotées par le scroll épingle un viewport avec `position: sticky` sur un conteneur défilant, convertit l'avancement du scroll en trait SVG circulaire via useScroll et useTransform de Framer Motion, et active chaque carte d'étape quand la fraction de scroll dépasse le seuil correspondant.
- Stack : React 18 + Framer Motion 11 + Tailwind v4, ~270 lignes, zéro dépendance supplémentaire.
- API clé : useScroll (target + offset), useTransform, motion.circle avec strokeDashoffset.
- La hauteur du conteneur s'adapte au nombre d'étapes (100vh par étape, minimum 200vh) pour que la distance de scroll correspond directement à la progression.
- Accessible : le contenu des étapes est toujours dans le DOM ; l'anneau et les marqueurs sont du SVG décoratif sans ARIA manquant.
- Fonctionne sur mobile : le scroll est disponible sur tous les appareils, aucun pointeur requis.
Video Scroll Play est une section React sticky qui transforme le scrollbar en indicateur de progression. En faisant défiler un conteneur haut, un anneau SVG circulaire se remplit de 0 à 100% et chaque carte d'étape s'illumine successivement. C'est un pattern répandu dans les flux d'onboarding et les sections "comment ça marche" où la vitesse de lecture ne doit pas dicter le rythme.
Anatomie
Le `<section>` extérieur est la cible du scroll ; sa hauteur est calculée depuis le nombre d'étapes pour que chaque étape occupe une hauteur de viewport. À l'intérieur, un div `position: sticky` se fixe en haut du viewport en permanence et occupe tout l'écran. Ce div sticky contient une grille à deux colonnes : la colonne gauche porte le titre, le sous-titre et la liste de cartes d'étapes ; la colonne droite est un SVG de 280px qui affiche un anneau décoratif extérieur, un cercle de piste et un arc de progression animé. Des marqueurs d'étapes (points de 8px sur l'anneau extérieur) passent de la couleur bordure à la couleur accent au fur et à mesure du scroll.
Comment ça marche
useScroll reçoit la ref de la section et un offset `['start start', 'end end']`, produisant un scrollYProgress de 0 à 1 couvrant tout le conteneur. useTransform mappe cette valeur sur le strokeDashoffset : la circonférence du cercle (2πr avec r=54) devient le dashoffset de départ et décrémente vers 0 à mesure que la page défile. Un second useTransform convertit la même plage 0-1 en pourcentage de 0 à 100 affiché au centre de l'anneau. La mise en surbrillance des étapes utilise un autre useTransform qui mappe la fraction de scroll linéairement en index d'étape, puis chaque carte vérifie si Math.round(currentIndex) correspond à son propre index pour basculer l'opacité, la couleur de bordure et le fond.
Comment le coder en React
Créer le conteneur de scroll et le viewport sticky
Donne à la section extérieure une hauteur proportionnelle au nombre d'étapes pour que chaque étape ait sa propre fenêtre de scroll. Imbrique un div `position: sticky; top: 0; height: 100vh` à l'intérieur, c'est le seul élément que l'utilisateur voit.
const sectionRef = useRef<HTMLElement>(null); // 100vh per step, min 200vh const sectionHeight = `${Math.max(200, steps.length * 100)}vh`;Relier la progression du scroll à l'arc SVG
Passe la ref de la section à useScroll avec l'offset `['start start', 'end end']`. Convertis scrollYProgress en strokeDashoffset en utilisant la circonférence complète comme valeur de départ. Applique la motion value directement sur un élément motion.circle.
const { scrollYProgress } = useScroll({ target: sectionRef, offset: ["start start", "end end"], }); const circumference = 2 * Math.PI * 54; const progressOffset = useTransform( scrollYProgress, [0, 1], [circumference, 0] ); // In JSX: // <motion.circle style={{ strokeDashoffset: progressOffset }} ... />Mapper la fraction de scroll sur l'étape active
useTransform peut mapper un tableau de valeurs d'entrée sur un tableau de valeurs de sortie. Alimente-le avec des fractions régulièrement espacées (une par étape) et les index d'étapes correspondants. Chaque carte dérive ensuite son état actif avec Math.round.
const currentStepIndex = useTransform( scrollYProgress, steps.map((_, i) => i / steps.length), steps.map((_, i) => i), ); // Per card: const isActive = useTransform(currentStepIndex, (v) => Math.round(v) === i);Animer les cartes d'étapes avec des motion values dérivées
Passe isActive (une MotionValue<boolean>) par de nouveaux useTransform pour obtenir opacity, couleur de bordure et couleur de fond. Applique-les en styles motion inline sur chaque carte, zéro re-render, tout géré dans le thread d'animation.
<motion.div style={{ opacity: useTransform(isActive, (v) => (v ? 1 : 0.35)), borderLeftColor: useTransform(isActive, (v) => v ? "var(--color-accent)" : "var(--color-border)" ), }} >
Quand l'utiliser
Utilise ce pattern sur les landing pages produit qui doivent expliquer un process multi-étapes sans mur de texte, flux d'onboarding, explications de fonctionnalités SaaS, guides d'installation. Fonctionne mieux avec 3 à 5 étapes ; moins de steps gaspillent le scroll disponible, plus de steps rendent le conteneur inconfortablement haut sur mobile. À éviter sur les pages axées conversion où la friction scroll-vers-CTA compte, et sur les pages où les utilisateurs arrivent en milieu de scroll via des liens d'ancre (l'anneau sera dans un état inattendu).
Utilisé par
- Stripe, Utilise des sections sticky à progression par étapes pour expliquer les flux de paiement et l'onboarding développeur.
- Notion, Emploie des séquences d'étapes verrouillées au scroll sur ses pages produit pour présenter la configuration d'un espace de travail.
- Linear, Visites de fonctionnalités pilotées par le scroll qui mettent en valeur chaque capacité à mesure que l'utilisateur défile dans la section marketing.
- Lottie by LottieFiles, Animations contrôlées par le scroll liées à la progression de la page pour démontrer pas à pas le fonctionnement de leur format d'animation.
FAQ
Pourquoi la hauteur de la section est-elle fixée à steps.length × 100vh ?
Chaque étape a besoin d'une plage de scroll équivalente pour être au premier plan. Avec 100vh par étape, défiler d'une hauteur de viewport passe d'une étape à la suivante. Utilise moins de vh par étape pour un rythme plus rapide, plus pour une lecture plus lente.
Puis-je remplacer l'anneau circulaire par une barre de progression linéaire ?
Oui. Remplace le SVG par un div, relie sa largeur ou son scaleX à la même motion value scrollYProgress via useTransform, et le reste de la logique reste identique.
Fonctionne-t-il sur mobile ?
L'interaction de scroll fonctionne sur tous les appareils y compris les écrans tactiles, useScroll suit le scroll initié au toucher de la même façon qu'au souris. La grille à deux colonnes passe en colonne unique via la classe Tailwind responsive.
Comment ajouter une vraie vidéo à la place de l'anneau ?
Mappe scrollYProgress sur la propriété currentTime d'un élément vidéo HTML via un useEffect qui lit la motion value. La vidéo scrubbing au scroll est une technique séparée : tu as besoin d'une vidéo préchargée et de requestAnimationFrame pour synchroniser la tête de lecture sans causer de layout thrash.