KPI tile Composition rather than a component. A tile is shadcn's Card plus the three pieces this library already specifies—the delta with its window, the freshness of the data, and a status the viewer can read at a glance. npx shadcn@latest add card Tokens this needs: --foreground, --muted, --muted-foreground, --border, --status-nominal, --chart-1, --direction-up The status, chart, scale, state and direction names are an extension, not a rename. shadcn has --destructive and five --chart-* and nothing else in this territory. ──────────────────────────────────────────────────────────────────────── // KpiTile.tsx ──────────────────────────────────────────────────────────────────────── import { Card } from "@/components/ui/card"; import { Delta } from "../delta-indicator/Delta"; import { Sparkline } from "../sparkline/Sparkline"; import { freshness, FRESHNESS_COLOR } from "../freshness-indicator/freshness"; import type { Polarity } from "../red-green-direction/direction"; /** * A tile is a composition, not a primitive. * * shadcn gives you the Card. What it cannot give you is the rule that a number * without context is a decoration: the viewer needs to know which way it moved, * against what, and how old it is. So `delta` and `lastArrival` are required * props rather than optional garnish, and a tile that cannot supply them should * be a plain figure instead of pretending to be a KPI. */ export function KpiTile({ label, value, delta, window, polarity = "higher-is-better", lastArrival, expectedEveryMs, now, trend, }: { label: string; value: string; delta: number; window: string; polarity?: Polarity; lastArrival: Date; expectedEveryMs: number; /** The clock to age against. Defaults to now; pass one to render on a server. */ now?: Date; /** The shape that got here: the third context signal, beside the delta. */ trend?: number[]; }) { const { state } = freshness(lastArrival, expectedEveryMs, now); return (

{label}

{value}

{trend && (
)}
); } ──────────────────────────────────────────────────────────────────────── // demo.tsx ──────────────────────────────────────────────────────────────────────── import { KpiTile } from "./KpiTile"; /** * One tile with its context signals: the delta, the trend, the window, and * the freshness dot. The clock is fixed so the dot renders the same on the * server and in the browser. */ const NOW = new Date("2026-09-15T09:00:00Z"); export default function Demo() { return (
); }