Cómo crear barras de progreso animadas al hacer scroll en React
Para crear barras de progreso animadas al hacer scroll en React, usa useInView de Framer Motion para detectar cuándo la sección entra en el viewport y luego controla el relleno de cada barra con un useSpring conectado a un useMotionValue. El mismo spring alimenta un contador de porcentaje en tiempo real mediante useTransform.
- Stack: React 18 + Framer Motion 11 + propiedades CSS personalizadas, ~240 líneas, cero dependencias adicionales.
- API principal: useMotionValue, useSpring (damping 20, stiffness 60), useTransform, useInView.
- Cada barra se anima en secuencia con un escalonamiento de 100ms (retardo index * 100ms) después de que la sección entra en vista.
- Accesible: los valores son texto legible; el relleno de la barra es una capa decorativa que no requiere ARIA.
- Responsive: el diseño dividido de dos columnas se adapta con fluidez en pantallas estrechas mediante CSS grid.
Metrics Animated Bars es una sección React de layout dividido que convierte datos en porcentaje en algo que ves suceder. Cuando la sección aparece al hacer scroll, las barras horizontales se rellenan de izquierda a derecha con una animación de física de spring mientras un contador en vivo sube hasta el valor final. El layout coloca una columna de título a la izquierda y la lista de barras a la derecha, dando a cada lado espacio para respirar.
Anatomía
La sección es una grid CSS con dos columnas iguales. La columna izquierda contiene un badge opcional, un título h2 y un párrafo de subtítulo, que aparecen juntos al hacer scroll. La columna derecha contiene la lista de barras: cada elemento es un subcomponente MetricBar con una fila de etiqueta (texto de la etiqueta, descripción opcional y el valor de porcentaje en vivo) sobre un track de 6px de alto que aloja dentro la capa de relleno animada.
Cómo funciona
Un único hook useInView vigila el contenedor de la columna derecha con un margen de -80px, disparándose una sola vez. Cuando inView pasa a true, cada MetricBar lanza un setTimeout retardado por index * 100ms y luego llama a motionVal.set(metric.value). El motionVal alimenta un useSpring (damping 20, stiffness 60) que se aproxima al valor objetivo con un ligero rebote elástico. useTransform mapea la salida del spring a scaleX entre 0 y 1, lo que controla la escala del div de relleno mediante transformOrigin:'left'. Un useTransform aparte convierte el mismo spring en una cadena de porcentaje para el contador, suscrito mediante displayVal.on('change') para mantenerlo sincronizado con la barra.
Cómo crearlo en React
Configura el motion value y el spring
Dentro de MetricBar, crea un useMotionValue que empiece en 0 y pásalo por useSpring. El damping y el stiffness del spring controlan lo elástico que se siente el relleno. Un stiffness más alto da una barra más viva; un damping más bajo prolonga el rebote.
const motionVal = useMotionValue(0); const spring = useSpring(motionVal, { damping: 20, stiffness: 60 });Controla el relleno y el contador desde el mismo spring
Usa dos llamadas useTransform sobre el spring: una que mapea [0, 100] a [0, 1] para el scaleX del div de relleno, y otra que formatea el valor como una cadena de porcentaje para el contador. Suscríbete a la transformación de cadena con .on('change') para mantener un estado React sincronizado.
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]);Dispara al hacer scroll con un retardo escalonado
Coloca una ref en el div contenedor y pásala a useInView con once:true y un margen de -80px para que la animación se dispare antes de que la sección entre por completo en el viewport. Cuando inView pasa a true, usa un setTimeout con index * 100ms antes de establecer el motion value, para que cada barra se rellene en secuencia en lugar de todas a la vez.
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]);Aplica scaleX al relleno con transformOrigin a la izquierda
Renderiza un motion.div dentro del track con position:absolute e inset:0, establece transformOrigin a 'left' y vincula el motion value scaleX directamente a la prop style. La barra crece entonces de izquierda a derecha a medida que el valor del spring sube.
<motion.div style={{ position: "absolute", inset: 0, backgroundColor: "var(--color-accent)", transformOrigin: "left", scaleX, }} />
Cuándo usarlo
Recurre a este componente en páginas de Acerca de, Servicios o Casos de éxito donde quieras mostrar datos de experiencia o rendimiento de forma visual y fácil de escanear. Funciona mejor con 4 a 8 métricas que tengan de forma natural una representación en porcentaje, como niveles de competencia, puntuaciones de satisfacción del cliente o tasas de cumplimiento de objetivos. Evítalo cuando los datos no sean naturalmente un porcentaje, cuando las métricas necesiten valores decimales precisos, o cuando los usuarios deban comparar barras entre varios conjuntos de datos a la vez, una biblioteca de gráficos real gestiona mejor esos casos.
Usado por
- Stripe, Usa indicadores animados de tipo progreso en el onboarding de su dashboard para mostrar las tasas de finalización de la configuración.
- Webflow, Emplea barras animadas horizontales en sus páginas de precios y comparación de funciones para destacar las capacidades de los planes.
- Figma, Muestra barras de relleno animadas en sus secciones de estadísticas de comunidad y uso de plugins.
FAQ
¿Por qué useSpring en lugar de una simple llamada a animate()?
useSpring aporta al relleno una inercia física: la barra se pasa ligeramente y se estabiliza, lo que se percibe como movimiento natural en lugar de un tween mecánico. Puedes ajustar damping y stiffness para adaptarlo a la energía de la marca sin tocar ninguna duración ni curva de easing.
¿Se pueden mostrar valores que no sean porcentajes?
El componente espera valores en el rango de 0 a 100 para el relleno de la barra. Si tus datos usan otra escala, normalízalos a ese rango antes de pasarlos, y ajusta el useTransform de displayVal para formatear la etiqueta como necesites (moneda, puntuación, etc.).
¿Cómo evito que la animación se vuelva a reproducir en cada re-render?
El hook useInView se llama con once:true, así que se dispara una sola vez cuando el contenedor entra por primera vez en el viewport. Los re-renders posteriores del padre no lo reinician. Si necesitas volver a reproducirla, desmonta y vuelve a montar el componente.
¿El escalonamiento falla si se renderizan muchas barras?
Cada barra añade 100ms de retardo, así que con 8 barras la última empieza después de 700ms. Eso sigue siendo perceptible y agradable. A partir de 10 barras el retardo acumulado se hace notorio; considera reducir el escalonamiento a la mitad, a 50ms, o limitar el retardo a un valor máximo.