Animated Number
animated-numberuiTweened number display — counts up on mount and smoothly retargets when the value changes. Ease-out cubic via requestAnimationFrame, custom formatter (Intl-ready), delay/stagger support, honors prefers-reduced-motion, SSR-safe.
Installation
$pnpm dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/animated-number.json$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/animated-number.json$yarn dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/animated-number.json$bunx shadcn-svelte@latest add https://uipkge.dev/r/svelte/animated-number.jsonManifest:
https://uipkge.dev/r/svelte/animated-number.jsonInstalls to:src/lib/components/ui/animated-number/Examples
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
value | number | — | required |
fromValue the first animation starts from. | number | 0 | optional |
durationms per tween. | number | 900 | optional |
delayms before the first tween starts. | number | 0 | optional |
format | (value: number) => string | — | optional |
disabledRender the target value instantly, no tween. | boolean | false | optional |
ref | HTMLSpanElement | null | — | optional |
Files installed (2)
src/lib/components/ui/animated-number/AnimatedNumber.svelte3.1 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface AnimatedNumberProps extends HTMLAttributes<HTMLSpanElement> { value: number /** Value the first animation starts from. */ from?: number /** ms per tween. */ duration?: number /** ms before the first tween starts. */ delay?: number format?: (value: number) => string /** Render the target value instantly, no tween. */ disabled?: boolean ref?: HTMLSpanElement | null } </script> <script lang="ts"> import { onDestroy, onMount, untrack } from 'svelte' import { cn } from '$lib/utils' /** * Tweened number display. Counts up on mount, then smoothly retargets * from the currently displayed value whenever `value` changes. * Server render outputs the final value so hydration always matches. */ let { value, from = 0, duration = 900, delay = 0, format, disabled = false, class: className, ref = $bindable(null), ...restProps }: AnimatedNumberProps = $props() const defaultFormat = (v: number) => String(Math.round(v)) // Snapshot once: the server render outputs the final value so hydration // matches; the mount tween below takes over from there. const getInitialDisplay = () => value let display = $state(getInitialDisplay()) let raf = 0 let startTimer: number | undefined function cancel() { if (raf) cancelAnimationFrame(raf) raf = 0 if (startTimer !== undefined) { clearTimeout(startTimer) startTimer = undefined } } function tweenTo(target: number, animateFrom: number, withDelay: boolean) { cancel() const reduce = typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches if (disabled || reduce) { display = target return } const startTime = performance.now() const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3) const run = () => { const t = Math.min(1, (performance.now() - startTime) / duration) display = animateFrom + (target - animateFrom) * easeOutCubic(t) raf = t < 1 ? requestAnimationFrame(run) : 0 } if (withDelay && delay > 0) { startTimer = window.setTimeout(() => { startTimer = undefined run() }, delay) } else { run() } } onMount(() => { // First paint showed the final value (SSR-safe); snap to `from`, then animate. tweenTo(value, from, true) }) let firstRun = true $effect(() => { const next = value // The mount tween above owns the first run; this effect only retargets // on later changes. `display` is read untracked so tween frames don't // resubscribe and restart the tween. if (firstRun) { firstRun = false return } const animateFrom = untrack(() => display) tweenTo(next, animateFrom, false) }) onDestroy(cancel) const formatted = $derived((format ?? defaultFormat)(display)) </script> <span bind:this={ref} data-uipkge="" data-slot="animated-number" class={cn('tabular-nums', className)} {...restProps}> {formatted} </span>src/lib/components/ui/animated-number/index.ts0.1 kB
export { default as AnimatedNumber, type AnimatedNumberProps } from './AnimatedNumber.svelte'
Raw manifest:https://uipkge.dev/r/svelte/animated-number.json