Retour au catalogue

Process Sticky Steps

Layout 2 colonnes : sidebar gauche sticky avec numero d'etape actif en crossfade (AnimatePresence) et barre de progression scroll-driven (scaleY). Droite : etapes qui se revelent au whileInView. Style Apple/Linear.

processcomplex Both Responsive a11y
minimalelegantcorporatesaasagencyuniversalsplitsticky
Theme

Créer une section process avec sidebar sticky en React

Une section process à sidebar sticky en React utilise useScroll et useTransform de Framer Motion pour relier la progression du scroll à une barre scaleY, et useMotionValueEvent pour dériver l'indice de l'étape active. La sidebar reste fixe pendant que les cartes d'étapes défilent à droite.

  • Stack : React 18 + Framer Motion 11 + CSS custom properties, ~108 lignes, zéro dépendance icône.
  • APIs Framer Motion : useScroll, useTransform, useMotionValueEvent, AnimatePresence.
  • Le numéro et le titre de l'étape active passent en crossfade via AnimatePresence mode='wait', évitant le chevauchement lors des transitions.
  • Les indicateurs en points animent leur largeur (6px à 20px) pour signaler l'étape active sans texte.
  • La sidebar sticky se compresse naturellement sur petits écrans, prévoir un layout une colonne en dessous du breakpoint md.

Cette section process couple une sidebar gauche sticky avec une colonne d'étapes défilantes. Au scroll, la sidebar suit la progression via une barre scaleY et permute le numéro d'étape actif en crossfade. Le résultat ressemble à un walkthrough 'comment ça marche' façon Apple ou Linear : structuré, calme et guidant l'attention.

Anatomie

La section extérieure contient un conteneur en grille deux colonnes : une sidebar gauche de 220px et une colonne droite flex. La sidebar est sticky (top : 30vh) et comprend une piste de progression (2px, remplissage accent scaleY), un compteur d'étape animé, un titre d'étape animé et une rangée de points indicateurs. La colonne droite affiche un StepCard par étape, chacun avec une carte à grille de points, un séparateur horizontal, un titre, un paragraphe de description et un detail en retrait.

Comment ça marche

Un containerRef encapsule la grille deux colonnes. useScroll cible cette ref avec offset ['start 0.6', 'end 0.6'], donc scrollYProgress passe de 0 à 1 quand la section défile devant la marque à 60% du viewport. useTransform mappe cette valeur 0-1 en une motion value scaleY pour le remplissage de la barre de progression. useMotionValueEvent écoute scrollYProgress à chaque changement et calcule l'indice de l'étape active en Math.floor(v * steps.length), clampé dans les bornes valides. AnimatePresence mode='wait' effectue ensuite le crossfade du numéro et du titre dans la sidebar. Chaque StepCard utilise whileInView avec un margin viewport de -15% pour n'entrer qu'une fois bien visible.

Comment le coder en React

  1. Mettre en place le conteneur scroll et dériver scrollYProgress

    Attache une ref à la div en grille deux colonnes et passe-la à useScroll. Le tuple offset indique à Framer Motion quand démarrer et terminer la plage de progression, 'start 0.6' se déclenche quand le haut du conteneur passe à 60% de la hauteur du viewport.

    const containerRef = useRef<HTMLDivElement>(null);
    const { scrollYProgress } = useScroll({
      target: containerRef,
      offset: ["start 0.6", "end 0.6"],
    });
  2. Mapper la progression sur le remplissage scaleY et l'indice de l'étape active

    useTransform convertit la valeur de progression 0-1 en un scaleY pour la barre accent. useMotionValueEvent se déclenche à chaque tick de scroll et calcule l'étape active avec un simple floor + clamp. Stocker activeStep dans useState re-rend la sidebar proprement sans affecter l'écouteur de scroll.

    const scaleY = useTransform(scrollYProgress, [0, 1], [0, 1]);
    const [activeStep, setActiveStep] = useState(0);
    
    useMotionValueEvent(scrollYProgress, "change", (v) => {
      setActiveStep(Math.max(0, Math.min(Math.floor(v * steps.length), steps.length - 1)));
    });
  3. Faire le crossfade du compteur d'étapes avec AnimatePresence

    Encapsule le numéro animé et le titre chacun dans leur propre AnimatePresence avec mode='wait'. Donne à chaque motion.span une key égale à activeStep pour que Framer Motion démonte l'ancien élément avant de monter le nouveau. L'animation de sortie (y: -20) et d'entrée (y: 20 vers 0) créent un effet de ticker fluide.

    <AnimatePresence mode="wait">
      <motion.span
        key={activeStep}
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        exit={{ opacity: 0, y: -20 }}
        transition={{ duration: 0.35 }}
      >
        {steps[activeStep]?.number}
      </motion.span>
    </AnimatePresence>
  4. Animer les cartes d'étapes à l'entrée du viewport

    Chaque StepCard enveloppe son contenu dans une motion.div avec initial={{ opacity: 0, y: 40 }} et whileInView={{ opacity: 1, y: 0 }}. Le margin viewport de -15% garantit que la carte n'entre qu'une fois réellement visible, pas juste au bord. Mettre once: true pour ne pas rejouer l'animation au scroll retour.

    <motion.div
      initial={{ opacity: 0, y: 40 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, margin: "-15%" }}
      transition={{ duration: 0.65, ease: [0.16, 1, 0.3, 1] }}
    >

Quand l'utiliser

Utiliser cette section quand tu as 3 à 5 étapes distinctes qui profitent d'un reveal rythmé, flows d'onboarding, méthodologies de service, workflows produit. Elle fonctionne mieux au milieu d'une page après une section features, pour montrer comment quelque chose fonctionne concrètement. À éviter quand les étapes n'ont pas d'ordre naturel, quand il y en a plus de 5 (le numéro sidebar perd son sens), ou quand tes utilisateurs sont principalement sur mobile (le layout deux colonnes demande une gestion responsive soignée).

Utilisé par

  • Linear, Utilise des sidebars synchronisées au scroll et des callouts sticky pour parcourir son workflow de suivi d'issues sur le site marketing.
  • Stripe, Plusieurs pages produit utilisent une colonne gauche sticky avec un panneau droit défilant pour expliquer les processus d'intégration en plusieurs étapes.
  • Notion, La section 'Comment ça marche' sur sa homepage utilise un compteur d'étapes sticky qui avance pendant que l'on défile dans les explications de fonctionnalités.
  • Loom, Révélation d'étapes scroll-driven avec un indicateur de progression persistant en sidebar pour guider les prospects dans la proposition de valeur produit.

FAQ

En quoi useMotionValueEvent diffère-t-il de useEffect pour suivre scrollYProgress ?

useEffect nécessiterait de créer un état dérivé via subscribe, déclenchant des re-renders React via un callback d'abonnement. useMotionValueEvent est un hook de première classe qui s'attache directement à l'événement change de la motion value, tourne en dehors du cycle de rendu React quand c'est possible, et évite les problèmes de closure périmée.

Pourquoi utiliser scaleY plutôt que height pour animer la barre de progression ?

Animer transform: scaleY s'exécute sur le thread compositor GPU et évite les recalculs de layout. Animer height force le navigateur à recalculer le layout à chaque frame, ce qui est nettement plus coûteux pendant un défilement rapide.

Que se passe-t-il avec seulement 2 étapes ou plus de 5 ?

Avec 2 étapes, la sidebar paraît creuse et l'effet sticky perd de son impact, une timeline horizontale est plus adaptée. Au-dessus de 5 étapes, la colonne droite devient très longue et le numéro en sidebar (01 à 08 par exemple) ne transmet plus de progression significative. Quatre étapes est le point optimal pour ce layout.

Comment gérer ce composant sur mobile ?

La grille deux colonnes avec sticky se casse sur petits viewports car 220px + gap + contenu dépasse la largeur d'écran. Passer à un layout une colonne empilée en dessous du breakpoint md (768px) : supprimer le sticky, masquer la sidebar ou la réduire à un simple compteur au-dessus de chaque carte.

"use client";

import { useRef, useState } from "react";
import { motion, useScroll, useTransform, useMotionValueEvent, AnimatePresence } from "framer-motion";

interface Step { number: string; title: string; description: string; detail: string; icon: string }
interface ProcessStickyStepsProps { eyebrow?: string; title?: string; subtitle?: string; steps?: Step[] }

const E: [number, number, number, number] = [0.16, 1, 0.3, 1];
const muted = "var(--color-foreground-muted)";
const fg = "var(--color-foreground)";
const accent = "var(--color-accent)";
const border = "var(--color-border)";

export default function ProcessStickySteps({
  eyebrow = "How it works",
  title = "A process built for clarity",
  subtitle = "Each step is designed to move you closer to the result with full transparency.",
  steps = [],
}: ProcessStickyStepsProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [activeStep, setActiveStep] = useState(0);

Code complet réservé à Pro

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

Passer en Pro, 9,99€/mois

Avis

Section process sticky scroll React, Tutoriel Framer Motion