Cómo crear una sección de estadísticas/números animada en React
Una sección de estadísticas animada en React usa whileInView de Framer Motion para activar un reveal escalonado (fade y slide) en cada tarjeta cuando la sección entra en el viewport. Cada tarjeta se anima de forma independiente con un retardo de 100ms para que aparezcan en cascada en lugar de todas a la vez.
- Stack: React + Framer Motion + Tailwind v4, ~88 líneas, cero dependencias adicionales.
- Animación: whileInView con viewport once:true, retardo escalonado de 100ms por tarjeta, easing spring personalizado [0.16, 1, 0.3, 1].
- Layout: CSS Grid dividido, bloque de texto a la izquierda, cuadrícula 2×2 de tarjetas a la derecha a partir del breakpoint lg.
- Totalmente tematizado mediante propiedades CSS personalizadas; sin colores codificados en ninguna parte.
- Accesible: section/h2 semánticos, valores de las estadísticas renderizados como texto plano (compatible con lectores de pantalla), sin trucos de ARIA.
About Numbers es una sección React que combina un bloque narrativo breve con una cuadrícula 2×2 de grandes tarjetas de estadísticas en color de acento. Cada tarjeta se anima al entrar en el viewport, de forma escalonada para que la cuadrícula parezca viva en lugar de una tabla estática. Cubre el patrón más habitual en sitios SaaS y de agencias: números que generan credibilidad de un solo vistazo.
Anatomía
La sección exterior usa propiedades CSS personalizadas para el padding y el ancho máximo, de modo que hereda los tokens de espaciado del proyecto. Dentro, un único CSS Grid se divide en dos columnas en el breakpoint lg: una columna izquierda con el encabezado h2 y un breve párrafo de descripción, y una columna derecha que es a su vez una cuadrícula de 2 columnas de tarjetas de estadísticas. Cada tarjeta es un contenedor redondeado con un fondo background-alt, un único border, el valor numérico en texto accent grande, y la etiqueta en mayúsculas pequeñas con tracking.
Cómo funciona
El bloque de texto se envuelve en un único motion.div con opacity 0 e y:16 en initial, activado por whileInView. Las tarjetas de estadísticas reciben el mismo tratamiento pero con un retardo por índice de i * 0.1 segundos, creando una cascada natural de izquierda a derecha y de arriba abajo. Todas las transiciones comparten un easing cubic-bezier personalizado [0.16, 1, 0.3, 1], una curva fast-out que resulta ágil sin ser brusca. La opción viewport once:true evita que la animación se repita al hacer scroll hacia arriba, manteniendo la experiencia limpia.
Cómo crearlo en React
Montar el layout de grid dividido
Envuelve todo en una section que lee el padding y el ancho máximo desde los tokens CSS. Dentro, usa un CSS Grid con grid-cols-1 en móvil y lg:grid-cols-2 en escritorio, con items-center para alinear verticalmente las dos mitades. Esta es la estructura base.
<section style={{ padding: "var(--section-padding-y)", background: "var(--color-background)" }}> <div className="mx-auto" style={{ maxWidth: "var(--container-max-width)" }}> <div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center"> {/* text block */} {/* stats grid */} </div> </div> </section>Animar el bloque de texto al hacer scroll
Envuelve el encabezado y el párrafo en un motion.div de Framer Motion. Establece initial en opacity:0 e y:16, y luego whileInView en opacity:1 e y:0. Pasa viewport={{ once: true }} para que la animación se dispare solo una vez. Usa la constante EASE personalizada para un resultado ágil.
const EASE = [0.16, 1, 0.3, 1] as const; <motion.div initial={{ opacity: 0, y: 16 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, ease: EASE }} > <h2>{title}</h2> <p>{description}</p> </motion.div>Construir las stat cards escalonadas
Recorre el array stats, envolviendo cada tarjeta en su propio motion.div. Pasa delay: i * 0.1 en la transition para que las tarjetas entren una tras otra. Cada tarjeta lee el border-radius, el fondo y el color del border desde los tokens CSS, sin valores codificados. El valor se renderiza en un span con el color accent, y la etiqueta en un span muted más pequeño.
{stats.map((stat, i) => ( <motion.div key={stat.label} initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.45, delay: i * 0.1, ease: EASE }} style={{ background: "var(--color-background-alt)", border: "1px solid var(--color-border)", borderRadius: "var(--radius-lg)", }} > <span style={{ color: "var(--color-accent)" }}>{stat.value}</span> <span style={{ color: "var(--color-foreground-muted)" }}>{stat.label}</span> </motion.div> ))}Aportar los datos de las estadísticas
Cada stat es un objeto con un string value y un string label. Los valores son strings preformateados como "12k+" o "99%", sin cálculos numéricos en runtime. Pásalos mediante la prop stats o reemplaza el mock por defecto con tus datos reales en el punto de uso.
const stats = [ { value: "12k+", label: "Clients" }, { value: "99%", label: "Satisfaction" }, { value: "8 ans", label: "Expérience" }, { value: "40+", label: "Pays" }, ]; <AboutNumbers title="Nos chiffres" description="..." stats={stats} />
Cuándo usarlo
Usa esta sección a media página en una landing de empresa, un sitio de marketing SaaS o un portafolio de agencia donde necesites generar confianza con números antes del CTA. Funciona mejor con 4 a 6 estadísticas de peso visual similar. Evítala cuando tus números aún no sean lo bastante impresionantes (productos en fase temprana) o cuando la página ya esté cargada de tablas de datos y gráficos: añadir otro bloque de números genera ruido.
Usado por
- Stripe, Usa grandes números en negrita en layouts divididos por sus páginas de inicio y de producto para destacar métricas de volumen y fiabilidad.
- Notion, Presenta una sección «acerca de» centrada en los números en su página de empresa con reveals de tarjetas escalonados.
- Intercom, Emplea bloques de estadísticas basados en cuadrícula a media página en sus páginas de marketing para generar credibilidad antes de la sección de precios.
- Webflow, Combina una breve narrativa de marca con una cuadrícula de tarjetas de métricas de plataforma en sus páginas de inicio y «acerca de».
FAQ
¿Cómo añado una animación de conteo de números (efecto count-up)?
La implementación actual muestra strings preformateados, lo que mantiene el componente simple y evita re-renders. Para añadir un count-up, reemplaza el span del valor por un componente que use useMotionValue de Framer Motion y anímalo de 0 hasta el número objetivo dentro de un useEffect activado por un IntersectionObserver o el callback de whileInView.
¿Puedo tener más de 4 stat cards?
La cuadrícula es grid-cols-2 sin un número fijo de filas, así que se adapta a cualquier número par de elementos. Un número impar dejará un hueco en la última fila. Con 6 tarjetas, el layout funciona bien. A partir de 8, considera cambiar a una fila con scroll horizontal o a una cuadrícula de 3 columnas en breakpoints más anchos.
¿Por qué usar whileInView en lugar de useEffect + IntersectionObserver?
whileInView es un wrapper ligero alrededor de IntersectionObserver integrado en Framer Motion. Como el componente ya depende de Framer Motion para la animación en sí, usar whileInView evita configurar un observer aparte y mantiene el código más corto. No hay diferencia de rendimiento.
¿Cómo cambio el color de acento de los valores de las estadísticas?
Los valores usan `color: var(--color-accent)`, que hereda del preset de tema activo. Cambia el atributo data-theme en un elemento padre para modificar toda la paleta, o sobrescribe la variable localmente con un style inline en la section si necesitas un color puntual.