shadimage
navui
uianimated-numberssource: ok · hit

Animated Numbers

Accessible in-view number counter with locale-aware formatting and reduced-motion support.

npx shadcn add "https://ui.navdeepsingh.dev/r/animated-numbers.json"
free

Where it goes

stats

Interaction

animated-counter

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
animationmotion

Files1 · source cached 10/9/2026, 6:05:24 PM

src/registry/components/animated-numbers/animated-numbers.tsx
'use client'

import type { ComponentPropsWithoutRef } from 'react'
import { useEffect, useMemo, useRef } from 'react'
import {
  animate,
  motion,
  useInView,
  useMotionValue,
  useReducedMotion,
  useTransform,
} from 'motion/react'

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

export type AnimatedNumberProps = Omit<ComponentPropsWithoutRef<'span'>, 'children'> & {
  value: number
  from?: number
  prefix?: string
  suffix?: string
  decimalPlaces?: number
  duration?: number
  delay?: number
  locale?: string
}

export function AnimatedNumber({
  value,
  from = 0,
  prefix = '',
  suffix = '',
  decimalPlaces = 0,
  duration = 1,
  delay = 0,
  locale = 'en-US',
  className,
  ...props
}: AnimatedNumberProps) {
  const ref = useRef<HTMLSpanElement>(null)
  const shouldReduceMotion = useReducedMotion()

  const isInView = useInView(ref, {
    once: true,
    amount: 0.6,
  })

  const animatedValue = useMotionValue(from)

  const formatter = useMemo(
    () =>
      new Intl.NumberFormat(locale, {
        minimumFractionDigits: decimalPlaces,
        maximumFractionDigits: decimalPlaces,
      }),
    [decimalPlaces, locale],
  )

  const formattedValue = useTransform(animatedValue, (currentValue) => {
    return `${prefix}${formatter.format(currentValue)}${suffix}`
  })

  const accessibleValue = `${prefix}${formatter.format(value)}${suffix}`

  useEffect(() => {
    if (shouldReduceMotion) {
      animatedValue.set(value)
      return
    }

    if (!isInView) return

    const controls = animate(animatedValue, value, {
      duration,
      delay,
      ease: [0.22, 1, 0.36, 1],
    })

    return () => controls.stop()
  }, [animatedValue, delay, duration, isInView, shouldReduceMotion, value])

  return (
    <span ref={ref} className={cn('inline-block tabular-nums', className)} {...props}>
      <motion.span aria-hidden="true">{formattedValue}</motion.span>
      <span className="sr-only">{accessibleValue}</span>
    </span>
  )
}

export default AnimatedNumber

More from navui

Browse all components in this library →