# Number Ticker

Each digit rolls on its own wheel to the new number, so changes read as motion instead of a flicker.

**Category:** Display  
**Exports:** `NumberTicker`  
**npm dependencies:** motion (+ clsx, tailwind-merge for `cn`)

## Install

```bash
npx shadcn@latest add https://charcoalui.com/r/number-ticker.json
```
The file lands at `components/ui/number-ticker.tsx`; import from `@/components/ui/number-ticker`.

## API — Number Ticker
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `value` | `number` | — |  |
| `format` | `Intl.NumberFormatOptions` | — |  |
| `locale` | `string` | `"en-US"` |  |

## Example — Number Ticker

```tsx
import * as React from "react";
import { NumberTicker } from "@/components/ui/number-ticker";
import { Button } from "@/components/ui/button";

export default function NumberTickerDemo({ label = "Monthly revenue", locale = "en-US" }: { label?: string; locale?: "en-US" | "de-DE" | "fr-FR" | "ja-JP" | "en-IN" }) {
  const [revenue, setRevenue] = React.useState(48210);
  return (
    <div className="flex flex-col items-center gap-6">
      <div className="text-center">
        <p className="text-sm text-muted-foreground">{label}</p>
        <NumberTicker value={revenue} locale={locale} format={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }} className="text-5xl font-semibold tracking-tight" />
      </div>
      <Button variant="secondary" size="sm" onClick={() => setRevenue((r) => Math.round(r + (Math.random() - 0.3) * 9000))}>
        New month
      </Button>
    </div>
  );
}
```

## Source — components/ui/number-ticker.tsx

```tsx
"use client";

import * as React from "react";
import { motion, useInView, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

export type NumberTickerProps = {
  value: number;
  format?: Intl.NumberFormatOptions;
  locale?: string;
  className?: string;
};

function Digit({ digit, show, reduce }: { digit: number; show: boolean; reduce: boolean | null }) {
  return (
    <span className="relative inline-block h-[1em] overflow-hidden leading-none">
      <span className="invisible">0</span>
      <motion.span
        className="absolute inset-x-0 top-0 flex flex-col"
        initial={false}
        animate={{ y: `${-(show ? digit : 0) * 10}%` }}
        transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 140, damping: 22, mass: 0.9 }}
      >
        {Array.from({ length: 10 }, (_, n) => (
          <span key={n} className="h-[1em] text-center leading-none">
            {n}
          </span>
        ))}
      </motion.span>
    </span>
  );
}

export function NumberTicker({ value, format, locale = "en-US", className }: NumberTickerProps) {
  const ref = React.useRef<HTMLSpanElement>(null);
  const inView = useInView(ref, { once: true, margin: "-10% 0px" });
  const reduce = useReducedMotion();
  const text = new Intl.NumberFormat(locale, format).format(value);
  const chars = text.split("");

  return (
    <span ref={ref} className={cn("inline-flex items-baseline tabular-nums", className)}>
      <span className="sr-only">{text}</span>
      <span aria-hidden className="inline-flex leading-none">
        {chars.map((ch, i) => {
          // Key from the right so a column keeps its identity as the number grows.
          const key = chars.length - i;
          return /\d/.test(ch) ? (
            <Digit key={key} digit={Number(ch)} show={inView || !!reduce} reduce={reduce} />
          ) : (
            <span key={`s${key}`} className="leading-none">
              {ch}
            </span>
          );
        })}
      </span>
    </span>
  );
}
```