Feedback & Data
CounterFlow
An odometer. Each digit is a strip of 0 to 9 that rolls into place the first time the number scrolls into view. Change the value later and only the digits that differ roll.
Download
- React
- Tailwind CSS v4
- motion
components/ui/counter-flow.tsx
"use client";
import { useRef } from "react";
import { motion, useInView, useReducedMotion } from "motion/react";
type CounterFlowProps = {
/** Final number. Change it later and only the digits that differ roll. */
value: number;
/** Text before the number, like "$". */
prefix?: string;
/** Text after the number, like "%" or "+". */
suffix?: string;
/** Digits after the point. */
decimals?: number;
/** Number format, like "en-IN" for lakh commas. */
locale?: string;
/** Seconds each digit rolls. */
duration?: number;
/** Wrapper look (text size, colour). */
className?: string;
};
const DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
function Digit({ digit, delay, duration }: { digit: number; delay: number; duration: number }) {
const reduce = useReducedMotion();
return (
<span className="relative inline-block h-[1.1em] w-[1ch] overflow-hidden">
<motion.span
className="absolute inset-x-0 top-0 flex flex-col text-center"
initial={{ y: "0%" }}
animate={{ y: `${-digit * 10}%` }}
transition={reduce ? { duration: 0 } : { duration, delay, ease: [0.16, 1, 0.3, 1] }}
>
{DIGITS.map((d) => (
<span key={d} className="h-[1.1em] leading-[1.1]">
{d}
</span>
))}
</motion.span>
</span>
);
}
/* CounterFlow: an odometer. Each digit is a strip of 0 to 9 that rolls into place the first time the number scrolls into view, right digit first. */
export function CounterFlow({ value, prefix, suffix, decimals = 0, locale = "en-US", duration = 1.4, className = "text-4xl font-semibold text-white" }: CounterFlowProps) {
const box = useRef<HTMLSpanElement>(null);
const seen = useInView(box, { once: true, amount: 0.5 });
const text = value.toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
const chars = [...text];
return (
<span ref={box} className={`inline-flex tabular-nums ${className}`}>
<span className="sr-only">{`${prefix ?? ""}${text}${suffix ?? ""}`}</span>
<span aria-hidden className="inline-flex leading-[1.1]">
{prefix}
{chars.map((char, i) => {
const fromRight = chars.length - i;
return /\d/.test(char) ? (
<Digit key={fromRight} digit={seen ? Number(char) : 0} delay={fromRight * 0.05} duration={duration} />
) : (
<span key={fromRight}>{char}</span>
);
})}
{suffix}
</span>
</span>
);
}
1. Install the dependency.
npm i motion2. Paste the Code tab into
components/ui/counter-flow.tsx. The styles are Tailwind CSS v4 classes.3. Use it. This is the preview above:
app/counter-flow-demo.tsx
"use client"; import { useState } from "react"; import { CounterFlow } from "@/components/ui/counter-flow"; const STAT = "text-2xl font-semibold text-white sm:text-3xl"; export default function CounterFlowExample() { const [users, setUsers] = useState(48210); return ( <div className="grid h-full place-items-center bg-[#0b0a14] p-5"> <div className="grid justify-items-center gap-5"> <div className="flex gap-6 sm:gap-8"> <div> <CounterFlow value={users} className={STAT} /> <p className="mt-1 text-[13px] text-white/50">Users</p> </div> <div> <CounterFlow value={99.98} decimals={2} suffix="%" className={STAT} /> <p className="mt-1 text-[13px] text-white/50">Uptime</p> </div> <div> <CounterFlow value={320} suffix="+" className={STAT} /> <p className="mt-1 text-[13px] text-white/50">Teams</p> </div> </div> <button type="button" onClick={() => setUsers((u) => u + 1250)} className="rounded-xl border border-white/10 bg-white/5 px-3.5 py-2 text-sm text-white/80 outline-none hover:bg-white/10 focus-visible:ring-2 focus-visible:ring-white/70" > Add 1,250 users </button> </div> </div> ); }
Props
| Name | Type | Default | What it does |
|---|---|---|---|
| value | number | โ | Final number. |
| prefix | string | โ | Text before the number, like "$". |
| suffix | string | โ | Text after the number, like "%" or "+". |
| decimals | number | 0 | Digits after the point. |
| locale | string | "en-US" | Number format, like "en-IN" for lakh commas. |
| duration | number | 1.4 | Seconds each digit rolls. |
| className | string | "text-4xl font-semibold text-white" | Text size and colour. |