Retour au catalogue

Stats Slot Machine

Les chiffres des statistiques défilent comme des colonnes de slot machine au scroll. Chaque digit individuel est une colonne 0-9 animée avec spring physics (rebond final satisfaisant). Stagger de droite à gauche pour un effet cascade élégant.

statscomplex Both Responsive a11y
boldelegantcorporatesaasagencyuniversalgridcentered
Theme

How to build a slot machine stats counter in React

A slot machine stats counter in React renders each digit as an independent scrolling column of 0-9, animated with a Framer Motion spring. On scroll-into-view, each column springs to its target digit with a right-to-left stagger, so units settle first and the effect reads like a real slot machine resolving.

  • Stack: React 18 + Framer Motion 11, ~308 lines, zero extra dependencies beyond framer-motion.
  • Core API: useSpring (stiffness 60, damping 18, mass 1.2), useTransform, useInView, with per-digit 80ms stagger delays.
  • Supports any string format, '10,000+', '99%', '48h', non-digit characters are rendered inline, digits get their own animated column.
  • Grid layout auto-adjusts columns: 1-2 stats use exact count, 3 stats = 3 columns, 4+ stats = 4 columns.
  • Accessible: static text values remain in the DOM; the animation is purely visual via CSS transforms.

Stats Slot Machine is a scroll-triggered React stats section where every individual digit in a number spins down a column of 0-9, snapping to its target with a springy rebound. The stagger fires right-to-left so the units digit lands first, making a multi-digit number read like a real slot machine resolving. It pairs well with a SaaS hero or features grid when you need social proof that earns a second glance.

Anatomy

The component has three levels. The outer section handles layout tokens (background, section padding, container max-width). A centered header area holds an optional badge, h2 title and subtitle paragraph, all fading in as a unit. Below it, a CSS grid of StatCard components adapts its column count to the number of stats passed. Each card has a subtle top accent line (a gradient fade from transparent to the accent color and back), the animated number in large bold type, a short label, and an optional description line.

How it works

A single useInView ref on the grid container fires when the grid crosses the viewport threshold (margin -80px). That boolean propagates to every DigitColumn. Each column maintains its own useSpring(0, { stiffness: 60, damping: 18, mass: 1.2 }) value and uses useTransform to convert it to a CSS translateY percentage (-v * 10%), scrolling a stacked column of ten digits (0 through 9). When inView turns true, a setTimeout fires with a computed delay: the rightmost digit starts immediately, each position to the left adds 80ms, and each card in the grid adds a further 120ms base delay. When inView turns false (never in this one-shot setup), spring.jump(0) resets without animation. Non-digit characters bypass the column entirely and render as static inline spans.

How to build it in React

  1. Tokenize the value string

    Split the stat value character by character. Each character becomes a token tagged as digit or non-digit. This lets '10,000+' produce animated columns for the six digits while the comma and plus sign render as plain text, preserving the exact formatting without any regex or replace logic.

    function tokenize(value: string) {
      return value.split("").map((char) => {
        const digit = parseInt(char, 10);
        return { char, isDigit: !isNaN(digit), digit: isNaN(digit) ? 0 : digit };
      });
    }
  2. Build the DigitColumn

    Each animated digit is a small clipping box (height: 1em, overflow: hidden) containing a motion.span that stacks all ten digits vertically. A useSpring drives the y transform from 0 to the target digit value, translating by -v * 10% to scroll the correct digit into view. The spring parameters (stiffness 60, damping 18, mass 1.2) produce a satisfying slow-in, bouncy stop.

    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]);
  3. Stagger right-to-left across digits and cards

    In SlotMachineNumber, count only the digit tokens and assign each one a delay that decreases as the index approaches the right side: baseDelay + (digits.length - 1 - localIndex) * 80. Pass each card's grid index as baseDelay = index * 120 so cards themselves stagger too.

    const staggerDelay = baseDelay + (digits.length - 1 - localIndex) * 80;
  4. Hook useInView to the grid container

    Attach a single ref to the wrapping grid div. Pass it to useInView with once: true and margin: '-80px' so the animation fires just before the grid fully enters the viewport. All DigitColumns read this one boolean, no need for individual IntersectionObservers per stat.

    const gridRef = useRef<HTMLDivElement>(null);
    const inView = useInView(gridRef, { once: true, margin: "-80px" });

When to use it

Use this section to anchor credibility with specific numbers on a SaaS, agency or corporate landing page, placed after the hero or a features block. The animation is deliberate and eye-catching, so one instance per page is enough. Skip it on pages with multiple animated sections competing for attention, and avoid it for numbers that update in real time (the animation is one-shot). Numbers with three or more digits benefit most from the stagger effect; single-digit stats lose most of the drama.

Used by

  • Stripe, Uses animated number counters in stats sections on its marketing pages to surface payment volume and uptime figures.
  • Lottiefiles, Displays community size and asset counts with scroll-triggered rolling number animations on its homepage.
  • Webflow, Stat blocks with staggered animated numbers appear across its enterprise and pricing pages.

FAQ

Why does the stagger go right-to-left instead of left-to-right?

The units digit (rightmost) carries the most visual weight in a large number, so it settling first creates a satisfying payoff moment. Left-to-right reads as mechanical because the most significant digit leading feels like loading, not resolving.

Can I use this for numbers that change dynamically (live counters)?

The current implementation is one-shot: inView fires once and the spring jumps back with spring.jump(0) rather than animating. For a live counter you would remove the once:true flag on useInView, drive the spring target from a data source, and skip the setTimeout delays.

How do I add a currency symbol or unit before the number?

Pass the full string including the prefix to the value prop, like '$10,000'. The tokenize function marks '$' as a non-digit, so it renders as a static inline span before the animated columns. No extra props or configuration needed.

Does the animation replay if the user scrolls away and back?

No. useInView is configured with once: true, so the animation fires exactly once per page load when the grid first enters the viewport. This avoids the jarring experience of stats re-rolling every time the user passes the section.

"use client";

import { useRef, useEffect } from "react";
import { motion, useInView, useSpring, useTransform } from "framer-motion";

interface StatItem {
  id: string;
  value: string;
  label: string;
  description?: string;
}

interface StatsSlotMachineProps {
  badge?: string;
  title?: string;
  subtitle?: string;
  stats?: StatItem[];
}

const EASE_OUT = [0.16, 1, 0.3, 1] as const;

// Splits a string like "10,000+" into character tokens, preserving non-digit chars

Code complet réservé à Pro

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

Passer en Pro, 9,99€/mois

Reviews

React Slot Machine Number Animation, Code + Tutorial