Velocity UI
Loading…
Menu

Number Ticker

Number Ticker component with smooth animations and modern UI.

Installation

Add this component to your project using the CLI:

terminal
npx vui-registry-cli-v1 add number-ticker

Source Code

number-ticker.tsx
'use client'

import { useEffect, useRef, useState } from 'react'
import { useInView, useMotionValue, useSpring, useVelocity, useTransform, motion } from 'framer-motion'
import { cn } from '@/lib/utils'

export default function NumberTickerPreview() {
  const [value, setValue] = useState(100)

  return (
    <div className="min-h-[400px] w-full flex flex-col items-center justify-center bg-neutral-100 dark:bg-neutral-950 p-8 space-y-12 font-sans">
        <div className="text-center space-y-4">
            <h2 className="text-6xl font-bold tracking-tighter text-neutral-800 dark:text-neutral-100 tabular-nums">
                $<NumberTicker value={value} />
            </h2>
            <p className="text-neutral-500 uppercase tracking-widest text-xs font-medium">Total Revenue</p>
        </div>

        <div className="flex gap-4">
            <button 
                onClick={() => setValue(v => v + Math.floor(Math.random() * 1000) + 100)}
                className="px-6 py-2.5 bg-neutral-900 dark:bg-white text-white dark:text-black rounded-full text-sm font-medium hover:scale-105 active:scale-95 transition-all shadow-lg"
            >
                Add Random
            </button>
            <button 
                onClick={() => setValue(v => Math.max(0, v - 500))}
                className="px-6 py-2.5 bg-neutral-200 dark:bg-neutral-800 text-neutral-900 dark:text-white rounded-full text-sm font-medium hover:bg-neutral-300 dark:hover:bg-neutral-700 transition-colors"
            >
                Subtract $500
            </button>
        </div>

        <div className="grid grid-cols-3 gap-12 text-center pt-8 border-t border-neutral-200 dark:border-neutral-800 w-full max-w-2xl">
            <div>
                <p className="text-xs font-medium text-neutral-400 uppercase tracking-wider mb-2">Active Users</p>
                <p className="text-3xl font-bold font-mono tracking-tight">
                    <NumberTicker value={8432} />
                </p>
            </div>
            <div>
                <p className="text-xs font-medium text-neutral-400 uppercase tracking-wider mb-2">Downloads</p>
                <p className="text-3xl font-bold font-mono tracking-tight text-indigo-500">
                    <NumberTicker value={1205} />k
                </p>
            </div>
            <div>
                <p className="text-xs font-medium text-neutral-400 uppercase tracking-wider mb-2">Satisfaction</p>
                <p className="text-3xl font-bold font-mono tracking-tight text-emerald-500">
                    <NumberTicker value={98} />%
                </p>
            </div>
        </div>
    </div>
  )
}

function NumberTicker({
  value,
  direction = 'up',
  delay = 0,
  className,
}: {
  value: number
  direction?: 'up' | 'down'
  className?: string
  delay?: number // delay in s
}) {
  const ref = useRef<HTMLSpanElement>(null)
  const motionValue = useMotionValue(direction === 'down' ? value : 0)
  const springValue = useSpring(motionValue, {
    damping: 60,
    stiffness: 100,
  })
  const isInView = useInView(ref, { once: true, margin: '0px' })

  useEffect(() => {
    if (isInView) {
      setTimeout(() => {
        motionValue.set(direction === 'down' ? 0 : value)
      }, delay * 1000)
    }
  }, [motionValue, isInView, delay, value, direction])

  useEffect(() => {
    const unsubscribe = springValue.on('change', (latest) => {
      if (ref.current) {
        ref.current.textContent = Intl.NumberFormat('en-US').format(
          Number(latest.toFixed(0)),
        )
      }
    })
    return unsubscribe
  }, [springValue])

  return (
    <span
      className={cn(
        'inline-block tabular-nums text-black dark:text-white tracking-wider',
        className,
      )}
      ref={ref}
    />
  )
}