# Number Ticker

The NumberTicker component counts a number up when it scrolls into view, with locale-aware formatting.

- **Category:** Text
- **Docs:** https://uibeats.com/docs/text/number-ticker
- **Registry:** https://uibeats.com/r/number-ticker.json
- **License:** MIT
- **npm dependencies:** `motion`

## Installation

```bash
npx shadcn@latest add https://uibeats.com/r/number-ticker.json
```

Or install the dependencies and copy the source below:

```bash
npm install motion
```

## When to use

For statistics that are the point of a section: users, uptime, revenue, downloads. The count implies growth, so it flatters a number meant to impress and is wasted on an ordinary one. Give it room to be read. A ticker inside a dense table is just a distraction.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `number` | `-` | The value to count to (required) |
| `from` | `number` | `0` | Where the count starts |
| `decimals` | `number` | `0` | Decimal places to display |
| `prefix` | `string` | `''` | Text placed before the number, e.g. a currency symbol |
| `suffix` | `string` | `''` | Text placed after the number, e.g. a percent sign |
| `once` | `boolean` | `true` | Count only the first time it scrolls into view |
| `locale` | `string` | `'en-US'` | BCP 47 locale used for grouping separators |
| `className` | `string` | `''` | Additional CSS classes for styling |

## Example usage

```tsx
"use client";
import { NumberTicker } from "@/components/demo/text/number-ticker";

const NumberTickerUsage = () => {
  return (
    <div className="flex flex-wrap items-center justify-center gap-10 text-center">
      <div>
        <NumberTicker value={12480} className="text-3xl font-bold" />
        <p className="mt-1 text-xs text-muted-foreground">Downloads</p>
      </div>
      <div>
        <NumberTicker
          value={99.9}
          decimals={1}
          suffix="%"
          className="text-3xl font-bold text-brand"
        />
        <p className="mt-1 text-xs text-muted-foreground">Uptime</p>
      </div>
    </div>
  );
};

export default NumberTickerUsage;
```

## Source

Save as `components/ui/number-ticker.tsx`.

```tsx
"use client";

import {
  useInView,
  useMotionValueEvent,
  useSpring,
  useReducedMotion,
} from "motion/react";
import { useEffect, useRef } from "react";

interface NumberTickerProps {
  /** The value to count to. */
  value: number;
  /** Where the count starts. */
  from?: number;
  decimals?: number;
  prefix?: string;
  suffix?: string;
  /** Count only the first time it scrolls into view. */
  once?: boolean;
  /** BCP 47 locale for grouping separators. */
  locale?: string;
  className?: string;
}

/**
 * A number that counts up when it scrolls into view.
 *
 * The animated digits are written straight to the DOM node rather than held
 * in state: a spring emits a value every frame, and re-rendering React sixty
 * times a second to print a number is wasted work.
 */
export function NumberTicker({
  value,
  from = 0,
  decimals = 0,
  prefix = "",
  suffix = "",
  once = true,
  locale = "en-US",
  className = "",
}: NumberTickerProps) {
  const ref = useRef<HTMLSpanElement>(null);
  const isInView = useInView(ref, { once, margin: "0px 0px -20% 0px" });
  const prefersReducedMotion = useReducedMotion();

  const spring = useSpring(from, {
    damping: 40,
    stiffness: 90,
    restDelta: decimals > 0 ? 0.001 : 0.5,
  });

  const format = (input: number) =>
    `${prefix}${new Intl.NumberFormat(locale, {
      minimumFractionDigits: decimals,
      maximumFractionDigits: decimals,
    }).format(input)}${suffix}`;

  useEffect(() => {
    if (!isInView) return;
    if (prefersReducedMotion) {
      spring.jump(value);
      return;
    }
    spring.set(value);
  }, [isInView, prefersReducedMotion, spring, value]);

  useMotionValueEvent(spring, "change", (latest) => {
    const node = ref.current;
    if (!node) return;
    // firstChild is the visible span; see the markup below.
    const visible = node.firstChild as HTMLElement | null;
    if (visible) visible.textContent = format(latest);
  });

  return (
    <span ref={ref} className={className}>
      {/* Only the settled value is announced. Without this, a screen reader
          would read out every intermediate number as the spring runs. */}
      <span aria-hidden="true" className="tabular-nums">
        {format(from)}
      </span>
      <span className="sr-only">{format(value)}</span>
    </span>
  );
}

export default NumberTicker;
```

## Credits

Written by [Claude Code](https://claude.com/claude-code).
