DHxWhy

Delta Number Ticker – Rolling Number with Animated Change Chip and Previous Value

Dashboards

About

Delta Number Ticker for React and Tailwind CSS (shadcn/ui): digits roll on every change while a chip with the absolute and percent change lifts off the number and settles beside it, and the old value lingers below as a ghost. It is built to show how much and which way a figure moved. Columns roll with a 20 ms stagger, new columns grow in for extra digits, and goodDirection paints a falling latency as good. Controlled previousValue or own memory, Intl formatting, polite announcements, reduced motion, light and dark. Use it for KPIs, prices, balances, latency and stock. The included demo plays a fixed storefront scenario and makes no requests. Requires React only, with no animation library.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Delta Number Ticker – Rolling Number with Animated Change Chip and Previous Value to your project and fits it to your theme.

Code

How Delta Number Ticker – Rolling Number with Animated Change Chip and Previous Value is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import * as React from "react";
import { DeltaNumberTicker } from "@/components/ui/delta-number-ticker";

type MetricKey = "revenue" | "sessions" | "latency";
type Values = { [key in MetricKey]: number };
type Step = { at: number; key: MetricKey; delta: number };
type Scene = { initial: Values; steps: Step[]; cycle: number; previous?: { [key in MetricKey]?: number }; seconds?: boolean };

const BASE_STEPS: Step[] = [
  { at: 600, key: "revenue", delta: 1240 },
  { at: 2000, key: "sessions", delta: -37 },
  { at: 3400, key: "latency", delta: -18 },
  { at: 4800, key: "revenue", delta: 880 },
  { at: 6200, key: "sessions", delta: 22 },
  { at: 7600, key: "latency", delta: 9 },
  { at: 9000, key: "revenue", delta: 1320 },
  { at: 10400, key: "sessions", delta: 41 },
  { at: 11800, key: "latency", delta: -6 },
  { at: 13200, key: "revenue", delta: 640 },
  { at: 14600, key: "sessions", delta: -26 },
  { at: 16000, key: "latency", delta: 15 },
];

const DEFAULT: Scene = { initial: { revenue: 82970, sessions: 1321, latency: 230 }, steps: BASE_STEPS, cycle: 16800 };

const SCENES: { [name: string]: Scene } = {
  digits: { initial: { revenue: 82970, sessions: 998, latency: 230 }, steps: [{ at: 600, key: "sessions", delta: 5 }, ...BASE_STEPS.slice(0, 1).map((step) => ({ ...step, at: 2000 }))], cycle: 4800 },
  shrink: { initial: { revenue: 82970, sessions: 1003, latency: 230 }, steps: [{ at: 600, key: "sessions", delta: -5 }, { at: 2000, key: "revenue", delta: 1240 }], cycle: 4800 },
  controlled: { initial: { revenue: 84210, sessions: 1321, latency: 230 }, steps: [{ at: 600, key: "revenue", delta: 2120 }, ...BASE_STEPS.slice(1)], cycle: 16800, previous: { revenue: 82970 } },
  flat: { initial: { revenue: 84210, sessions: 1321, latency: 230 }, steps: BASE_STEPS.slice(1), cycle: 16800, previous: { revenue: 84210 } },
  fromzero: { initial: { revenue: 82970, sessions: 12, latency: 230 }, steps: BASE_STEPS, cycle: 16800, previous: { sessions: 0 } },
  seconds: { initial: { revenue: 82970, sessions: 1321, latency: 3.4 }, steps: [{ at: 600, key: "latency", delta: 0.5 }, { at: 2000, key: "revenue", delta: 1240 }], cycle: 4800, seconds: true },
};

const EASE = "cubic-bezier(0.22, 1, 0.36, 1)";
const CURRENCY: Intl.NumberFormatOptions = { style: "currency", currency: "USD", minimumFractionDigits: 0, maximumFractionDigits: 0 };
const MILLISECONDS: Intl.NumberFormatOptions = { style: "unit", unit: "millisecond", unitDisplay: "short" };
const seconds = (value: number) => `${value.toFixed(1)}s`;
const round = (value: number) => Math.round(value * 100) / 100;
const clockLabel = (index: number) => {
  const minutes = 10 * 60 + 42 + index;
  return `${Math.floor(minutes / 60)}:${String(minutes % 60).padStart(2, "0")}`;
};

export default function Demo({ autoplay = true }: { autoplay?: boolean }) {
  const params = React.useMemo(() => new URLSearchParams(typeof window === "undefined" ? "" : window.location.search), []);
  const scene = React.useMemo(() => SCENES[params.get("scene") ?? ""] ?? DEFAULT, [params]);
  const [values, setValues] = React.useState(scene.initial);
  const [clock, setClock] = React.useState({} as { [key in MetricKey]?: string });
  const [stopped, setStopped] = React.useState(false);
  const [settled, setSettled] = React.useState([] as string[]);
  const [applied, setApplied] = React.useState(0);
  const [motion, setMotion] = React.useState({ ready: false, reduced: true });
  const [entered, setEntered] = React.useState(!autoplay);
  const stageRef = React.useRef(null as HTMLElement | null);
  const scripted = autoplay && !stopped;

  React.useEffect(() => {
    const media = window.matchMedia("(prefers-reduced-motion: reduce)");
    const update = () => setMotion({ ready: true, reduced: media.matches });
    update();
    media.addEventListener("change", update);
    return () => media.removeEventListener("change", update);
  }, []);

  React.useEffect(() => {
    if (!motion.ready || !autoplay) return;
    const node = stageRef.current;
    if (!motion.reduced && node && typeof node.animate === "function") {
      node.animate([{ opacity: 0, transform: "translateY(10px)" }, { opacity: 1, transform: "translateY(0)" }], { duration: 420, easing: EASE });
    }
    setEntered(true);
  }, [motion.ready, motion.reduced, autoplay]);

  React.useEffect(() => {
    if (!scripted) return;
    const start = performance.now();
    let index = 0;
    let cycle = 0;
    let timer = 0;
    const schedule = () => {
      const step = scene.steps[index];
      const at = cycle * scene.cycle + (step ? step.at : scene.cycle);
      timer = window.setTimeout(() => {
        if (step) {
          const ordinal = cycle * scene.steps.length + index;
          setValues((current) => ({ ...current, [step.key]: round(current[step.key] + step.delta) }));
          setClock((current) => ({ ...current, [step.key]: clockLabel(ordinal) }));
          setApplied((count) => count + 1);
          index += 1;
        } else {
          index = 0;
          cycle += 1;
        }
        schedule();
      }, Math.max(0, at - (performance.now() - start)));
    };
    schedule();
    return () => window.clearTimeout(timer);
  }, [scripted, scene]);

  const stop = () => { if (scripted) setStopped(true); };
  const settle = (key: MetricKey) => (value: number) => setSettled((list) => [...list, `${key}:${value}`]);
  const previous = (key: MetricKey) => (scene.previous && scene.previous[key] !== undefined ? { previousValue: scene.previous[key] } : {});

  return (
    <main
      data-demo-root
      data-stopped={stopped ? "true" : "false"}
      data-settled={settled.join(" ")}
      data-applied={applied}
      onPointerDownCapture={stop}
      onKeyDownCapture={stop}
      className="flex w-full items-center justify-center overflow-hidden bg-background px-6 text-foreground"
      style={{ height: "min(100svh, 760px)" }}
    >
      <section
        ref={stageRef}
        data-stage
        aria-label="Storefront overview"
        className="w-full max-w-[520px] rounded-2xl bg-card p-6 text-card-foreground shadow-sm ring-1 ring-border"
        style={{ opacity: entered ? 1 : 0 }}
      >
        <header className="flex items-center justify-between gap-4">
          <div className="min-w-0">
            <h2 className="text-sm font-semibold leading-5">Storefront overview</h2>
            <p className="text-xs leading-4 text-muted-foreground">Today · updates as orders land</p>
          </div>
          <span data-live-dot className="inline-flex shrink-0 items-center gap-1.5 text-xs font-medium text-muted-foreground">
            <span aria-hidden="true" className="size-1.5 rounded-full bg-chart-2" />
            Live
          </span>
        </header>
        <div className="mt-3 divide-y divide-border">
          <DeltaNumberTicker label="Revenue today" value={values.revenue} format={CURRENCY} changedAt={clock.revenue} onSettle={settle("revenue")} className="py-4" {...previous("revenue")} />
          <DeltaNumberTicker label="Active sessions" value={values.sessions} changedAt={clock.sessions} onSettle={settle("sessions")} className="py-4" {...previous("sessions")} />
          <DeltaNumberTicker
            label="p95 latency"
            value={values.latency}
            format={scene.seconds ? seconds : MILLISECONDS}
            goodDirection="down"
            changedAt={clock.latency}
            onSettle={settle("latency")}
            className="py-4"
            {...previous("latency")}
          />
        </div>
      </section>
    </main>
  );
}

Dependencies

More by DHxWhy

View all
  • Product Rail Gallery – Fashion Store Hero Section

    Product Rail Gallery – Fashion Store Hero Section

    2.2K
  • Sushi Conveyor Belt Menu – Restaurant Hero Section

    Sushi Conveyor Belt Menu – Restaurant Hero Section

    883
  • Team Lanyard Rail – Meet the Team Section with Flip Badges

    Team Lanyard Rail – Meet the Team Section with Flip Badges

    827
  • Skateboard Deck Rail Gallery – Skate Shop Hero Section

    Skateboard Deck Rail Gallery – Skate Shop Hero Section

    430
  • Sun Moon Dark Mode Theme Toggle

    Sun Moon Dark Mode Theme Toggle

    328
  • Picture Rail Image Gallery – Art Print Shop Hero Section

    Picture Rail Image Gallery – Art Print Shop Hero Section

    268
  • Pendant Lamp Rail Gallery – Lighting Store Hero Section

    Pendant Lamp Rail Gallery – Lighting Store Hero Section

    243
  • World Testimonials Globe – Social Proof Hero Section

    World Testimonials Globe – Social Proof Hero Section

    217

More in Dashboards

View all
  • Dashboard with Collapsible Sidebar

    Dashboard with Collapsible Sidebar

    2.5K
  • Sidebar

    Sidebar

    341
  • SaaS Template

    SaaS Template

    2.6K
  • Course Design Cards

    Course Design Cards

    1.2K
  • Stats Bento

    Stats Bento

    1.2K
  • Chat template

    Chat template

    639
  • Stats cards with links

    Stats cards with links

    128
  • Card

    Card

    231