Cómo crear un contador de stats tipo tragamonedas en React
Un contador de stats tipo tragamonedas en React renderiza cada dígito como una columna independiente de 0 a 9, animada con un spring de Framer Motion. Al entrar en vista con el scroll, cada columna rebota hacia su dígito objetivo con un stagger de derecha a izquierda, así las unidades se asientan primero y el efecto se percibe como una tragamonedas real resolviéndose.
- Stack: React 18 + Framer Motion 11, ~308 líneas, cero dependencias extra más allá de framer-motion.
- API principal: useSpring (stiffness 60, damping 18, mass 1.2), useTransform, useInView, con retardos de stagger de 80 ms por dígito.
- Admite cualquier formato de cadena, '10,000+', '99%', '48h', los caracteres no numéricos se renderizan inline y los dígitos reciben su propia columna animada.
- El layout de grid ajusta las columnas automáticamente: 1-2 stats usan el conteo exacto, 3 stats = 3 columnas, 4 o más stats = 4 columnas.
- Accesible: los valores de texto estáticos permanecen en el DOM; la animación es puramente visual mediante transforms CSS.
Stats Slot Machine es una sección de stats en React activada al hacer scroll donde cada dígito de un número gira bajando por una columna de 0 a 9, encajando en su objetivo con un rebote elástico. El stagger se dispara de derecha a izquierda para que el dígito de las unidades aterrice primero, dando a un número de varios dígitos la apariencia de una tragamonedas real resolviéndose. Combina bien con un hero de SaaS o una grid de features cuando necesitas prueba social que se gane una segunda mirada.
Anatomía
El componente tiene tres niveles. La sección exterior gestiona los tokens de layout (fondo, padding de sección, ancho máximo del contenedor). Una zona de encabezado centrada contiene un badge opcional, un título h2 y un párrafo de subtítulo, que aparecen juntos en fundido. Debajo, una grid CSS de componentes StatCard adapta su número de columnas a la cantidad de stats pasadas. Cada card tiene una fina línea de acento arriba (un fundido en degradado desde transparente hacia el color de acento y de vuelta), el número animado en tipografía grande en negrita, una etiqueta corta y una línea de descripción opcional.
Cómo funciona
Un único ref useInView en el contenedor de la grid se dispara cuando la grid cruza el umbral del viewport (margin -80px). Ese booleano se propaga a cada DigitColumn. Cada columna mantiene su propio useSpring(0, { stiffness: 60, damping: 18, mass: 1.2 }) y usa useTransform para convertirlo en un porcentaje CSS translateY (-v * 10%), desplazando una columna apilada de diez dígitos (0 al 9). Cuando inView pasa a true, un setTimeout se dispara con un retardo calculado: el dígito más a la derecha arranca de inmediato, cada posición hacia la izquierda suma 80 ms, y cada card de la grid añade además un retardo base de 120 ms. Cuando inView pasa a false (nunca en este montaje de un solo disparo), spring.jump(0) reinicia sin animación. Los caracteres no numéricos evitan la columna por completo y se renderizan como spans inline estáticos.
Cómo crearlo en React
Tokenizar la cadena de valor
Divide el valor de la stat carácter por carácter. Cada carácter se convierte en un token etiquetado como dígito o no-dígito. Esto permite que '10,000+' produzca columnas animadas para los seis dígitos mientras la coma y el signo más se renderizan como texto plano, conservando el formato exacto sin ninguna lógica de regex o replace.
function tokenize(value: string) { return value.split("").map((char) => { const digit = parseInt(char, 10); return { char, isDigit: !isNaN(digit), digit: isNaN(digit) ? 0 : digit }; }); }Construir la DigitColumn
Cada dígito animado es una pequeña caja de recorte (height: 1em, overflow: hidden) que contiene un motion.span que apila los diez dígitos verticalmente. Un useSpring impulsa el transform y de 0 al valor del dígito objetivo, trasladando -v * 10% para desplazar el dígito correcto a la vista. Los parámetros del spring (stiffness 60, damping 18, mass 1.2) producen una entrada lenta y una parada rebotante muy satisfactoria.
const spring = useSpring(0, { stiffness: 60, damping: 18, mass: 1.2 }); const y = useTransform(spring, (v) => `${-v * 10}%`); useEffect(() => { if (inView) { const id = setTimeout(() => spring.set(digit), delay); return () => clearTimeout(id); } else { spring.jump(0); } }, [inView, digit, delay, spring]);Escalonar de derecha a izquierda entre dígitos y cards
En SlotMachineNumber, cuenta solo los tokens de dígito y asigna a cada uno un retardo que disminuye a medida que el índice se acerca al lado derecho: baseDelay + (digits.length - 1 - localIndex) * 80. Pasa el índice de grid de cada card como baseDelay = index * 120 para que las cards también se escalonen.
const staggerDelay = baseDelay + (digits.length - 1 - localIndex) * 80;Conectar useInView al contenedor de la grid
Adjunta un único ref al div contenedor de la grid. Pásalo a useInView con once: true y margin: '-80px' para que la animación se dispare justo antes de que la grid entre por completo en el viewport. Todas las DigitColumns leen este único booleano, sin necesidad de IntersectionObservers individuales por stat.
const gridRef = useRef<HTMLDivElement>(null); const inView = useInView(gridRef, { once: true, margin: "-80px" });
Cuándo usarlo
Usa esta sección para anclar credibilidad con cifras concretas en una landing de SaaS, agencia o corporativa, colocada después del hero o de un bloque de features. La animación es deliberada y llamativa, así que una sola instancia por página es suficiente. Evítala en páginas con varias secciones animadas compitiendo por la atención, y descártala para cifras que se actualizan en tiempo real (la animación es de un solo disparo). Los números de tres o más dígitos aprovechan mejor el efecto de stagger; las stats de un solo dígito pierden casi todo el impacto.
Usado por
- Stripe, Usa contadores de números animados en secciones de stats de sus páginas de marketing para destacar los volúmenes de pago y las cifras de disponibilidad.
- Lottiefiles, Muestra el tamaño de su comunidad y los recuentos de assets con animaciones de números giratorios activadas al hacer scroll en su página de inicio.
- Webflow, Aparecen bloques de stats con números animados escalonados en sus páginas de empresa y de precios.
FAQ
¿Por qué el stagger va de derecha a izquierda en lugar de izquierda a derecha?
El dígito de las unidades (el más a la derecha) carga con el mayor peso visual en un número grande, así que verlo asentarse primero crea un momento de recompensa satisfactorio. De izquierda a derecha se percibe mecánico porque ver llegar primero el dígito más significativo da sensación de carga, no de resolución.
¿Puedo usarlo para números que cambian dinámicamente (contadores en vivo)?
La implementación actual es de un solo disparo: inView se dispara una vez y el spring vuelve con spring.jump(0) en lugar de animar. Para un contador en vivo tendrías que quitar el flag once:true en useInView, alimentar el objetivo del spring desde una fuente de datos y omitir los retardos de setTimeout.
¿Cómo añado un símbolo de moneda o una unidad antes del número?
Pasa la cadena completa incluyendo el prefijo a la prop value, como '$10,000'. La función tokenize marca '$' como no-dígito, así que se renderiza como un span inline estático antes de las columnas animadas. No se necesita ninguna prop ni configuración extra.
¿Se vuelve a reproducir la animación si el usuario hace scroll fuera y regresa?
No. useInView está configurado con once: true, así que la animación se dispara exactamente una vez por carga de página cuando la grid entra por primera vez en el viewport. Esto evita la experiencia molesta de ver las stats volver a girar cada vez que el usuario pasa por la sección.