UI

Components / Motion

Number Ticker

Free · MIT

Counts up to a number when it scrolls into view, with locale-aware formatting for currency, percentages and decimals.

v1.0.1 · Updated 2026-09-28

0

01Installation

$ pnpm dlx shadcn@latest add @ballmac/number-ticker
Install manually

Install the dependencies:

$ pnpm add motion@^12

Copy the source into your project:

components/ballmac/number-ticker.tsx
// Ballmac UI: Number Ticker. https://ui.ballmac.com/components/number-ticker
"use client"

import * as React from "react"
import { animate, useInView, useReducedMotion } from "motion/react"

import { cn } from "@/lib/utils"

type NumberTickerProps = Omit<React.ComponentProps<"span">, "children"> & {
  /** The number to count to. */
  value: number
  /** Where the count starts. Defaults to 0. */
  from?: number
  /** Seconds. Defaults to 1.4. */
  duration?: number
  /** Seconds to wait after the number scrolls into view. */
  delay?: number
  /** Intl.NumberFormat options, for example { style: "currency", currency: "USD" }. */
  format?: Intl.NumberFormatOptions
  /** BCP 47 locale. Fixed (not the browser's) so server and client render the same text. */
  locale?: string
}

function NumberTicker({
  value,
  from = 0,
  duration = 1.4,
  delay = 0,
  format,
  locale = "en-US",
  className,
  ...props
}: NumberTickerProps) {
  const ref = React.useRef<HTMLSpanElement>(null)
  const inView = useInView(ref, { once: true, margin: "0px 0px -10% 0px" })
  const reduceMotion = useReducedMotion()
  const formatter = React.useMemo(() => new Intl.NumberFormat(locale, format), [locale, format])

  React.useEffect(() => {
    const node = ref.current
    if (!node) return
    if (reduceMotion) {
      node.textContent = formatter.format(value)
      return
    }
    node.textContent = formatter.format(from)
    if (!inView) return
    const controls = animate(from, value, {
      duration,
      delay,
      ease: [0.22, 1, 0.36, 1],
      onUpdate: (latest) => {
        const decimals = format?.maximumFractionDigits ?? (Number.isInteger(value) ? 0 : 2)
        node.textContent = formatter.format(Number(latest.toFixed(decimals)))
      },
    })
    return () => controls.stop()
  }, [inView, reduceMotion, value, from, duration, delay, formatter, format?.maximumFractionDigits])

  return (
    <span
      ref={ref}
      data-slot="number-ticker"
      className={cn("inline-block tabular-nums tracking-tight", className)}
      {...props}
    >
      {formatter.format(reduceMotion ? value : from)}
    </span>
  )
}

export { NumberTicker, type NumberTickerProps }

02Usage

import { NumberTicker } from "@/components/ballmac/number-ticker"

03Examples

Currency

$0.00

04Options

  • format accepts any Intl.NumberFormat options
  • locale defaults to en-US so server and browser render identical text; pass your locale explicitly
  • duration and delay in seconds

05Dependencies

Registry
shadcn/utils

06Use with AI

Animated number for stats and KPIs. Respects reduced motion by showing the final value immediately. With the shadcn MCP server set up (guide), ask your agent:

Add the Ballmac UI Number Ticker (@ballmac/number-ticker) to this project with the shadcn MCP, then use it where it fits.

Use it for

  • Stat sections on landing pages
  • Dashboard KPIs that load in

Not for

  • Numbers that update every second (animate a live value instead)
  • Prices users need to read instantly

Registry JSON: https://ui.ballmac.com/r/number-ticker.json

07Source

components/ballmac/number-ticker.tsx
// Ballmac UI: Number Ticker. https://ui.ballmac.com/components/number-ticker
"use client"

import * as React from "react"
import { animate, useInView, useReducedMotion } from "motion/react"

import { cn } from "@/lib/utils"

type NumberTickerProps = Omit<React.ComponentProps<"span">, "children"> & {
  /** The number to count to. */
  value: number
  /** Where the count starts. Defaults to 0. */
  from?: number
  /** Seconds. Defaults to 1.4. */
  duration?: number
  /** Seconds to wait after the number scrolls into view. */
  delay?: number
  /** Intl.NumberFormat options, for example { style: "currency", currency: "USD" }. */
  format?: Intl.NumberFormatOptions
  /** BCP 47 locale. Fixed (not the browser's) so server and client render the same text. */
  locale?: string
}

function NumberTicker({
  value,
  from = 0,
  duration = 1.4,
  delay = 0,
  format,
  locale = "en-US",
  className,
  ...props
}: NumberTickerProps) {
  const ref = React.useRef<HTMLSpanElement>(null)
  const inView = useInView(ref, { once: true, margin: "0px 0px -10% 0px" })
  const reduceMotion = useReducedMotion()
  const formatter = React.useMemo(() => new Intl.NumberFormat(locale, format), [locale, format])

  React.useEffect(() => {
    const node = ref.current
    if (!node) return
    if (reduceMotion) {
      node.textContent = formatter.format(value)
      return
    }
    node.textContent = formatter.format(from)
    if (!inView) return
    const controls = animate(from, value, {
      duration,
      delay,
      ease: [0.22, 1, 0.36, 1],
      onUpdate: (latest) => {
        const decimals = format?.maximumFractionDigits ?? (Number.isInteger(value) ? 0 : 2)
        node.textContent = formatter.format(Number(latest.toFixed(decimals)))
      },
    })
    return () => controls.stop()
  }, [inView, reduceMotion, value, from, duration, delay, formatter, format?.maximumFractionDigits])

  return (
    <span
      ref={ref}
      data-slot="number-ticker"
      className={cn("inline-block tabular-nums tracking-tight", className)}
      {...props}
    >
      {formatter.format(reduceMotion ? value : from)}
    </span>
  )
}

export { NumberTicker, type NumberTickerProps }

Released under the MIT License.