Back

NumberFlow

Click to randomize the number

Key Props

valuenumber — the value to animate
formatIntl.NumberFormatOptions
localeslocale string(s) for formatting
prefix / suffixstatic text around the number
transformTiming{ duration, easing } for layout
spinTiming{ duration, easing } for digit spin
opacityTiming{ duration, easing } for fade
trend1 | -1 | 0 | function — spin direction
plugins[continuous] for fluid scrolling
willChangeboolean — optimize for animation
isolateboolean — prevent layout shift
digitsper-digit max constraints

Powered by NumberFlow by @mbarvian

Component Code (Next.js)

"use client";

import { useState, useEffect, useCallback } from "react";
import NumberFlow, { NumberFlowGroup } from "@number-flow/react";
import { continuous } from "number-flow/plugins";

// --- Basic ---
function BasicDemo() {
  const [value, setValue] = useState(321);
  return (
    <button onClick={() => setValue(Math.floor(Math.random() * 10000))}>
      <NumberFlow
        value={value}
        className="text-5xl font-semibold"
        transformTiming={{ duration: 500, easing: "ease-out" }}
      />
    </button>
  );
}

// --- Currency ---
function CurrencyDemo() {
  const [price, setPrice] = useState(1234.56);
  const [currency, setCurrency] = useState("USD");
  return (
    <NumberFlow
      value={price}
      format={{ style: "currency", currency, trailingZeroDisplay: "stripIfInteger" }}
      className="text-4xl font-semibold"
      transformTiming={{ duration: 500, easing: "ease-out" }}
    />
  );
}

// --- Percent ---
function PercentDemo() {
  const [value, setValue] = useState(0.856);
  return (
    <>
      <NumberFlow
        value={value}
        format={{ style: "percent", minimumFractionDigits: 1 }}
        className="text-4xl font-semibold"
      />
      <input type="range" min={0} max={100} value={Math.round(value * 100)}
        onChange={(e) => setValue(+e.target.value / 100)} />
    </>
  );
}

// --- Compact Notation ---
function CompactDemo() {
  const [value, setValue] = useState(1_234_567);
  return (
    <NumberFlow
      value={value}
      format={{ notation: "compact", compactDisplay: "short", maximumFractionDigits: 1 }}
      className="text-4xl font-semibold"
      willChange
    />
  );
}

// --- Countdown (NumberFlowGroup) ---
function CountdownDemo() {
  const [total, setTotal] = useState(3661);
  useEffect(() => {
    const id = setInterval(() => setTotal((p) => (p <= 0 ? 3661 : p - 1)), 1000);
    return () => clearInterval(id);
  }, []);
  const hh = Math.floor(total / 3600);
  const mm = Math.floor((total % 3600) / 60);
  const ss = total % 60;
  return (
    <NumberFlowGroup>
      <div style={{ fontVariantNumeric: "tabular-nums" }} className="flex items-baseline text-5xl font-semibold">
        <NumberFlow trend={-1} value={hh} format={{ minimumIntegerDigits: 2 }} />
        <NumberFlow prefix=":" trend={-1} value={mm} digits={{ 1: { max: 5 } }} format={{ minimumIntegerDigits: 2 }} />
        <NumberFlow prefix=":" trend={-1} value={ss} digits={{ 1: { max: 5 } }} format={{ minimumIntegerDigits: 2 }} />
      </div>
    </NumberFlowGroup>
  );
}

// --- Input stepper ---
function InputDemo() {
  const [value, setValue] = useState(5);
  return (
    <div className="flex items-center">
      <button onClick={() => setValue((v) => Math.max(0, v - 1))}>\u2212</button>
      <NumberFlow value={value} className="text-4xl font-semibold" />
      <button onClick={() => setValue((v) => Math.min(99, v + 1))}>+</button>
    </div>
  );
}

// --- Continuous plugin ---
function ContinuousDemo() {
  const [value, setValue] = useState(50);
  return (
    <>
      <NumberFlow
        value={value}
        plugins={[continuous]}
        willChange isolate
        className="text-5xl font-semibold"
        transformTiming={{ duration: 750, easing: "ease-out" }}
        opacityTiming={{ duration: 250, easing: "ease-out" }}
      />
      <input type="range" min={0} max={100} value={value}
        onChange={(e) => setValue(+e.target.value)} />
    </>
  );
}

// --- Trend ---
function TrendDemo() {
  const [value, setValue] = useState(42);
  return (
    <NumberFlow
      value={value}
      trend={(oldVal, newVal) => Math.sign(newVal - oldVal)}
      className="text-5xl font-semibold"
    />
  );
}

Claude / Codex Prompt

Build a NumberFlow showcase page in Next.js using @number-flow/react (by Maxwell Barvian) with 8 interactive variants in tabs:

1. Basic — click to randomize a number with smooth digit transitions
2. Currency — format as USD/EUR/GBP/JPY with currency switcher pills
3. Percent — slider-controlled percentage with minimumFractionDigits
4. Compact — compact notation (1.2M, 56.8K) with preset value buttons
5. Countdown — live HH:MM:SS countdown using NumberFlowGroup to sync three NumberFlow instances, with trend={-1} and digits constraints
6. Input — stepper with +/- buttons, animated number in the center
7. Continuous — continuous plugin for fluid intermediate number scrolling, controlled by a range slider
8. Trend — custom trend function that spins digits up/down based on value direction, with increase/decrease buttons

Requirements:
- Install: npm install @number-flow/react
- Use tabular-nums for countdown alignment
- All demos responsive (text-3xl on mobile, text-5xl on desktop)
- Each variant in its own tab pill
- Wrap in ComponentShell with credit link
- Credit: https://number-flow.barvian.me by @mbarvian