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

Cómo crear una sección process con sidebar sticky en React

Una sección process con sidebar sticky en React usa useScroll y useTransform de Framer Motion para vincular el progreso del scroll a una barra scaleY, y useMotionValueEvent para derivar el índice del paso activo. El sidebar permanece fijo mientras las tarjetas de paso pasan por la derecha.

  • Stack: React 18 + Framer Motion 11 + CSS custom properties, ~108 líneas, cero dependencias de iconos.
  • APIs principales de Framer Motion: useScroll, useTransform, useMotionValueEvent, AnimatePresence.
  • El número y el título del paso activo hacen crossfade mediante AnimatePresence en mode='wait', evitando el solapamiento durante las transiciones.
  • Los indicadores de puntos animan su anchura (de 6px a 20px) para señalar el paso activo sin texto.
  • El sidebar sticky se contrae de forma natural en pantallas pequeñas, conviene pasar a un diseño de una sola columna por debajo del breakpoint md.

Esta sección process combina un sidebar izquierdo sticky con una columna de tarjetas de paso desplazable. Mientras el usuario hace scroll, el sidebar sigue el progreso con una barra scaleY y cambia el número de paso activo mediante un crossfade suave. El resultado se lee como un recorrido 'cómo funciona' al estilo de Apple o Linear: estructurado, tranquilo y que dirige la atención.

Anatomía

La sección exterior contiene un contenedor de ancho completo dividido en un sidebar izquierdo de 220px y una columna derecha en flex-column. El sidebar es sticky (top: 30vh) e incluye una pista de progreso (2px de ancho, relleno scaleY con color de acento), un contador de paso animado, un título de paso animado y una fila de indicadores de puntos. La columna derecha renderiza un StepCard por paso, cada uno con una tarjeta decorativa de cuadrícula de puntos, una línea horizontal, un título, un párrafo de descripción y un detalle destacado con sangría.

Cómo funciona

Un containerRef envuelve la cuadrícula de dos columnas. useScroll apunta a esa ref con offset ['start 0.6', 'end 0.6'], por lo que scrollYProgress va de 0 a 1 mientras la sección pasa la marca del 60% del viewport. useTransform mapea ese valor de 0 a 1 a un motion value scaleY para el relleno de la barra de progreso. useMotionValueEvent escucha scrollYProgress en cada cambio y calcula el índice del paso activo como Math.floor(v * steps.length), limitado a los rangos válidos. AnimatePresence mode='wait' hace luego el crossfade del número y del título en el sidebar. Cada StepCard usa whileInView con un margin de viewport de -15% para que entre solo cuando está bien dentro del viewport.

Cómo crearlo en React

  1. Configurar el contenedor de scroll y derivar scrollYProgress

    Asigna una ref al div de la cuadrícula de dos columnas y pásala a useScroll. La tupla offset le indica a Framer Motion cuándo empezar y terminar el rango de progreso, 'start 0.6' se dispara cuando la parte superior del contenedor cruza el 60% de la altura del viewport.

    const containerRef = useRef<HTMLDivElement>(null);
    const { scrollYProgress } = useScroll({
      target: containerRef,
      offset: ["start 0.6", "end 0.6"],
    });
  2. Mapear el progreso al relleno scaleY y al índice del paso activo

    useTransform convierte el valor de progreso de 0 a 1 en un scaleY para la barra de acento. useMotionValueEvent se dispara en cada tick de scroll y calcula qué paso está activo con un simple floor + clamp. Almacenar activeStep en useState vuelve a renderizar el sidebar de forma limpia sin afectar al listener 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. Hacer el crossfade del contador de pasos con AnimatePresence

    Envuelve el número animado y el título cada uno en su propio AnimatePresence con mode='wait'. Asigna a cada motion.span una key igual a activeStep para que Framer Motion desmonte el elemento antiguo antes de montar el nuevo. La animación de salida (y: -20) y la de entrada (y: 20 hacia 0) crean una sensación fluida de ticker.

    <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. Animar la entrada de las tarjetas de paso con whileInView

    Cada StepCard envuelve su contenido en un motion.div con initial={{ opacity: 0, y: 40 }} y whileInView={{ opacity: 1, y: 0 }}. El margin de viewport de -15% garantiza que la tarjeta solo entre una vez que está realmente dentro del área visible, no justo en el borde. Pon once: true para que la animación no se repita al volver a hacer scroll.

    <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] }}
    >

Cuándo usarlo

Usa esta sección cuando tengas entre 3 y 5 pasos concretos que se beneficien de una revelación pausada, flujos de onboarding, metodologías de servicio, workflows de producto. Funciona mejor en el centro de una página después de una sección de features, dando contexto sobre cómo funciona algo realmente. Evítala cuando los pasos no tengan un orden natural, cuando tengas más de 5 pasos (el número del sidebar pierde sentido), o cuando tus usuarios sean principalmente mobile-first (el diseño de dos columnas requiere un manejo responsive cuidadoso).

Usado por

  • Linear, Usa sidebars sincronizados con el scroll y callouts de features sticky para recorrer su workflow de seguimiento de issues en el sitio de marketing.
  • Stripe, Varias páginas de producto usan una columna izquierda sticky con un panel derecho desplazable para explicar procesos de integración de varios pasos.
  • Notion, La sección 'Cómo funciona' de su homepage usa un contador de pasos sticky que avanza mientras haces scroll por las explicaciones de features.
  • Loom, Revelación de pasos guiada por el scroll con un indicador de progreso persistente en el sidebar para guiar a los prospectos por la propuesta de valor del producto.

FAQ

¿En qué se diferencia useMotionValueEvent de useEffect para seguir scrollYProgress?

useEffect requeriría crear un estado derivado mediante subscribe, lo que provoca re-renders de React a través de un callback de suscripción. useMotionValueEvent es un hook de primera clase que se engancha directamente al evento change del motion value, se ejecuta fuera del ciclo de render de React cuando es posible, y evita los problemas de closure obsoleto.

¿Por qué usar scaleY en lugar de height para animar la barra de progreso?

Animar transform: scaleY se ejecuta en el hilo compositor de la GPU y evita recálculos de layout. Animar height obliga al navegador a recalcular el layout en cada frame, lo que es notablemente más costoso durante un scroll rápido.

¿Qué pasa si solo hay 2 pasos o más de 5?

Con 2 pasos el sidebar se ve vacío y el truco sticky pierde impacto, una timeline horizontal funciona mejor. Por encima de 5 pasos la columna derecha desplazable se vuelve muy larga y el número del sidebar (01 a 08, por ejemplo) deja de transmitir un progreso significativo. Cuatro pasos es el punto óptimo para este diseño.

¿Cómo debo gestionar este componente en móvil?

La cuadrícula de dos columnas con sticky se rompe en viewports pequeños porque 220px + gap + contenido supera el ancho de pantalla. Pasa a un diseño apilado de una sola columna por debajo del breakpoint md (768px): quita el posicionamiento sticky, oculta el sidebar o redúcelo a un simple contador de paso encima de cada tarjeta.

"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

Reseñas

Sección process sticky scroll en React, tutorial Framer