Components / Motion
Number Ticker
Free · MITCounts 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
import { NumberTicker } from "@/components/ballmac/number-ticker"
export default function NumberTickerDemo() {
return <NumberTicker value={12840} className="text-5xl font-semibold" />
}01Installation
$ pnpm dlx shadcn@latest add @ballmac/number-tickerInstall manually
Install the dependencies:
$ pnpm add motion@^12Copy 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
import { NumberTicker } from "@/components/ballmac/number-ticker"
export default function NumberTickerCurrency() {
return (
<NumberTicker
value={48250.5}
format={{ style: "currency", currency: "USD", maximumFractionDigits: 2 }}
className="text-5xl font-semibold"
/>
)
}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
- npm
- motion@^12
- 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.