Counter
Animated number counter.
Demo
0.0%
<script lang="ts"> import * as Null from "#lib/index.js"; </script> <Null.Counter value={40.4} decimals={1} suffix={"%"} />
Counter.svelte
<script lang="ts"> import { untrack } from "svelte"; type Props = { value: number; duration?: number; decimals?: number; prefix?: string; suffix?: string; locale?: string; }; let { value, duration = 800, decimals = 0, prefix = "", suffix = "", locale = "en-US", }: Props = $props(); let displayed = $state(0); let formatter = $derived( new Intl.NumberFormat(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals, }), ); let formatted = $derived(prefix + formatter.format(displayed) + suffix); $effect(() => { const target = value; const ms = Math.max(0, duration); const from = untrack(() => displayed); if (!Number.isFinite(target)) { displayed = target; return; } if (ms === 0 || window.matchMedia("(prefers-reduced-motion: reduce)").matches) { displayed = target; return; } const start = performance.now(); let frame = 0; function tick(now: number) { const progress = Math.min((now - start) / ms, 1); const eased = 1 - Math.pow(1 - progress, 4); displayed = from + (target - from) * eased; if (progress < 1) { frame = requestAnimationFrame(tick); } else { displayed = target; } } frame = requestAnimationFrame(tick); return () => cancelAnimationFrame(frame); }); </script> <span aria-label={`${prefix}${formatter.format(value)}${suffix}`}> {formatted} </span>