Skip to content

Instantly share code, notes, and snippets.

@josephdburdick
Created October 5, 2025 04:33
Show Gist options
  • Select an option

  • Save josephdburdick/a71f81349b1577e7c7e0c1350c927c4a to your computer and use it in GitHub Desktop.

Select an option

Save josephdburdick/a71f81349b1577e7c7e0c1350c927c4a to your computer and use it in GitHub Desktop.
Animate number up or down with motion/react
'use client'
import { useEffect, useRef, useState } from 'react'
import { cn } from '@/utils/cn'
interface AnimateNumberProps {
from?: number
to: number
duration?: number
delay?: number
className?: string
formatter?: (value: number) => string
ease?: string
onComplete?: () => void
}
export function AnimateNumber({
from = 0,
to,
duration = 1.5,
delay = 0,
className,
formatter = (value) => Math.round(value).toString(),
onComplete,
}: AnimateNumberProps) {
const [displayValue, setDisplayValue] = useState(from)
const animationRef = useRef<number | null>(null)
const lastTargetRef = useRef<number | null>(null) // Initialize with null instead
const displayValueRef = useRef(displayValue)
// Update the ref whenever displayValue changes
displayValueRef.current = displayValue
useEffect(() => {
// Only start animation if target value actually changed (or this is the first render)
if (lastTargetRef.current !== null && lastTargetRef.current === to) {
return
}
// Cancel any existing animation
if (animationRef.current !== null) {
cancelAnimationFrame(animationRef.current)
}
const timeout = setTimeout(() => {
// Set the target ref here, after we're committed to starting the animation
lastTargetRef.current = to
const startTime = Date.now()
const startValue = displayValueRef.current // Use ref instead of closure
const difference = to - startValue
const animate = () => {
const elapsed = Date.now() - startTime
const progress = Math.min(elapsed / (duration * 1000), 1)
// Simple easeOut function
const easeProgress = 1 - Math.pow(1 - progress, 3)
const currentValue = startValue + difference * easeProgress
setDisplayValue(currentValue)
if (progress < 1) {
animationRef.current = requestAnimationFrame(animate)
} else {
setDisplayValue(to)
onComplete?.()
animationRef.current = null
}
}
animationRef.current = requestAnimationFrame(animate)
}, delay * 1000)
return () => {
clearTimeout(timeout)
if (animationRef.current !== null) {
cancelAnimationFrame(animationRef.current)
}
}
}, [to, duration, delay, onComplete])
return (
<span className={cn('tabular-nums tracking-tighter', className)}>
{formatter(displayValue)}
</span>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment