animate(0 → n) · 1.6s · tabular-nums
Number Counter
useMotionValue + animate で KPI 数値を補間し、useTransform でフォーマットする。tabular-nums で桁の reflow を防ぐのが肝。duration は 1–2 秒が読みやすい。
MRR
¥0+12.4% MoM
Active users
0last 28 days
Conversion
0.00%−0.3pt WoW
/**
* Number Counter — useMotionValue + animate で KPI 数値を補間する。
* 移植元: motion-playground app/_sections/NumberCounter.tsx(dark 変換 M-B-2)
*/
import { animate, motion, useMotionValue, useTransform } from 'motion/react';
import { useEffect } from 'react';
import { easings } from '../../../lib/motion-tokens/playground';
import { useLabReplay } from './use-lab-replay';
function Counter({
to,
duration,
format,
playKey,
}: {
to: number;
duration: number;
format: (n: number) => string;
playKey: number;
}) {
const value = useMotionValue(0);
const display = useTransform(value, (latest) => format(latest));
useEffect(() => {
value.set(0);
const controls = animate(value, to, {
duration,
ease: easings.md3Standard,
});
return () => controls.stop();
}, [to, duration, value, playKey]);
return (
<motion.span className="font-mono text-3xl font-semibold tabular-nums text-[var(--text-primary)] lg:text-4xl">
{display}
</motion.span>
);
}
const CELL =
'rounded-xl border border-[var(--border-subtle)] bg-[var(--bg-surface)] p-5';
const CELL_LABEL =
'mb-1 text-xs font-medium uppercase tracking-[0.1em] text-[var(--text-muted)]';
export default function NumberCounter() {
const { rootRef, playKey } = useLabReplay();
return (
<div
ref={rootRef}
data-lab-island
className="grid w-full gap-3 p-4 sm:grid-cols-3 lg:p-6"
>
<div className={CELL}>
<p className={CELL_LABEL}>MRR</p>
<Counter
to={48230}
duration={1.6}
format={(n) => `¥${Math.round(n).toLocaleString()}`}
playKey={playKey}
/>
<p className="mt-1 text-xs text-[var(--success)]">+12.4% MoM</p>
</div>
<div className={CELL}>
<p className={CELL_LABEL}>Active users</p>
<Counter
to={2348}
duration={1.6}
format={(n) => Math.round(n).toLocaleString()}
playKey={playKey}
/>
<p className="mt-1 text-xs text-[var(--text-muted)]">last 28 days</p>
</div>
<div className={CELL}>
<p className={CELL_LABEL}>Conversion</p>
<Counter
to={3.86}
duration={1.6}
format={(n) => `${n.toFixed(2)}%`}
playKey={playKey}
/>
<p className="mt-1 text-xs text-[var(--danger)]">−0.3pt WoW</p>
</div>
</div>
);
} Implement an animated KPI counter with motion/react:
- useMotionValue(0) + animate(value, target, { duration: 1.6, ease: cubic-bezier(0.2, 0, 0, 1) })
- format via useTransform (currency / thousands separator / percentage)
- use font-variant-numeric: tabular-nums to prevent digit reflow
- re-run by resetting the motion value when a replay key changes
- 1-2s duration reads best for dashboard KPIs