こんにちは世界

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>