OSAGI

KPI Tile

Dashboards 28

About

Compact KPI tile: a value that rolls to each new number, its change with an arrow that turns, and an SVG sparkline that draws in, morphs as data arrives and shows any day's value under the pointer or the arrow keys. Loading and empty states.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds KPI Tile to your project and fits it to your theme.

Code

How KPI Tile is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import { useEffect, useState } from "react";
import { KpiTile } from "@/components/ui/kpi-tile";

const settings = {
  /** Days in each sparkline, 2 to 30. */
  days: 14,
  /** Tiles with no data yet. */
  empty: false,
  /** A new day every two seconds. */
  live: true,
  /** Skeletons, as while the first numbers load. */
  loading: false,
};

const DAY_MS = 86_400_000;
/** The slow trend is a long wave, so a demo left running never drifts out of range. */
const TREND_DAYS = 60;
const FIRST_DAY = Date.UTC(2026, 9, 1);
const TICK_MS = 2000;
const DAY_NAME = new Intl.DateTimeFormat("en-US", {
  day: "numeric",
  month: "short",
  timeZone: "UTC",
});

type Metric = {
  label: string;
  base: number;
  /** Growth per day, and how far a day wobbles around it. */
  growth: number;
  swing: number;
  phase: number;
  format: (value: number) => string;
  lowerIsBetter?: boolean;
};

const whole = new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 });
const METRICS: Metric[] = [
  {
    base: 41_800,
    format: (value) => `$${whole.format(value)}`,
    growth: 0.011,
    label: "Revenue",
    phase: 0,
    swing: 0.05,
  },
  {
    base: 9640,
    format: (value) => whole.format(value),
    growth: 0.006,
    label: "Active users",
    phase: 1.6,
    swing: 0.03,
  },
  {
    base: 3.1,
    format: (value) => `${value.toFixed(2)}%`,
    growth: 0.003,
    label: "Conversion",
    phase: 3,
    swing: 0.07,
  },
  {
    base: 248,
    format: (value) => `${Math.round(value)} ms`,
    growth: -0.004,
    label: "p95 latency",
    lowerIsBetter: true,
    phase: 1.6,
    swing: 0.06,
  },
];

/** The same story on every visit: a slow trend with a repeatable wobble of two sines. */
const valueOn = (metric: Metric, day: number) =>
  metric.base *
  (1 + metric.growth * TREND_DAYS * Math.sin(day / TREND_DAYS)) *
  (1 +
    metric.swing *
      (Math.sin(day * 1.7 + metric.phase) * 0.6 +
        Math.sin(day * 0.6 + metric.phase * 2) * 0.4));

/** Four live KPIs: three that should grow, and a latency where lower is better. */
export default function Demo(props: Partial<typeof settings>) {
  const { days, empty, live, loading } = { ...settings, ...props };
  const count = Number.isFinite(days)
    ? Math.min(Math.max(Math.round(days), 2), 30)
    : 14;
  const [today, setToday] = useState(30);

  useEffect(() => {
    if (!live) {
      return;
    }
    const timer = window.setInterval(() => setToday((day) => day + 1), TICK_MS);
    return () => window.clearInterval(timer);
  }, [live]);

  const inView = Array.from(
    { length: count },
    (_, index) => today - count + 1 + index
  );
  const labels = inView.map((day) => DAY_NAME.format(FIRST_DAY + day * DAY_MS));

  return (
    <div className="flex min-h-[360px] w-full items-center justify-center bg-background p-4 sm:p-8">
      <div className="grid w-full max-w-5xl gap-4 sm:grid-cols-2 lg:grid-cols-4">
        {METRICS.map((metric) => {
          const trend = inView.map((day) => valueOn(metric, day));
          const now = trend.at(-1) ?? 0;
          const before = trend.at(-2) ?? now;
          return (
            <KpiTile
              delta={empty ? undefined : ((now - before) / before) * 100}
              deltaLabel="vs yesterday"
              format={metric.format}
              key={metric.label}
              label={metric.label}
              live={live}
              loading={loading}
              lowerIsBetter={metric.lowerIsBetter}
              trend={empty ? [] : trend}
              trendLabels={labels}
              value={empty ? undefined : now}
            />
          );
        })}
      </div>
    </div>
  );
}

Dependencies

More by OSAGI

View all
  • AI Orb

    AI Orb

    83
  • OTP Input

    OTP Input

    12
  • Reasoning Thread

    Reasoning Thread

    8
  • Assistant Composer

    Assistant Composer

    8
  • Release Notes

    Release Notes

  • Docs TOC

    Docs TOC

  • Docs Search

    Docs Search

  • Workload Timeline

    Workload Timeline

More in Dashboards

View all
  • Dashboard with Collapsible Sidebar

    Dashboard with Collapsible Sidebar

    2.4K
  • Sidebar

    Sidebar

    234
  • SaaS Template

    SaaS Template

    2.4K
  • Chat template

    Chat template

    598
  • Stats Bento

    Stats Bento

    1.1K
  • Financial Bento Grid

    Financial Bento Grid

    71
  • Statistics Card 2

    Statistics Card 2

    264
  • Course Design Cards

    Course Design Cards

    1.1K