Infrastructure & DevOps

Metric Tile

A big-number KPI tile with a filled sparkline, signed delta, and threshold-driven accent color.

Install

npx shadcn@latest add @paragon/metric-tile

metric-tile.tsx

"use client";

import * as React from "react";
import { ArrowDownRight, ArrowUpRight } from "lucide-react";
import { cn } from "@/lib/utils";

export type MetricThreshold = "ok" | "warn" | "critical";

export interface MetricTileProps extends React.ComponentProps<"div"> {
  label: string;
  value: string;
  unit?: string;
  /** Signed percentage delta vs the prior window, e.g. -4.2. */
  delta?: number;
  /** For delta color: is a rise good or bad? Latency rising is bad. */
  deltaGoodDirection?: "up" | "down";
  /** Sparkline series, oldest → newest. Normalized internally. */
  series?: number[];
  threshold?: MetricThreshold;
}

const THRESHOLD_VAR: Record<MetricThreshold, string> = {
  ok: "var(--color-success)",
  warn: "var(--color-warning)",
  critical: "var(--color-destructive)",
};

function Sparkline({ series, color }: { series: number[]; color: string }) {
  const w = 100;
  const h = 32;
  const min = Math.min(...series);
  const max = Math.max(...series);
  const span = max - min || 1;
  const step = series.length > 1 ? w / (series.length - 1) : w;
  const pts = series.map((v, i) => {
    const x = i * step;
    const y = h - ((v - min) / span) * (h - 4) - 2;
    return [x, y] as const;
  });
  const line = pts.map(([x, y], i) => `${i === 0 ? "M" : "L"} ${x.toFixed(1)} ${y.toFixed(1)}`).join(" ");
  const area = `${line} L ${w} ${h} L 0 ${h} Z`;
  const id = React.useId().replace(/[^a-zA-Z0-9-]/g, "");
  return (
    <svg
      viewBox={`0 0 ${w} ${h}`}
      preserveAspectRatio="none"
      className="h-8 w-full"
      aria-hidden
    >
      <defs>
        <linearGradient id={`spark-${id}`} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor={color} stopOpacity="0.18" />
          <stop offset="100%" stopColor={color} stopOpacity="0" />
        </linearGradient>
      </defs>
      <path d={area} fill={`url(#spark-${id})`} />
      <path
        d={line}
        fill="none"
        stroke={color}
        strokeWidth="1.5"
        strokeLinecap="round"
        strokeLinejoin="round"
        vectorEffect="non-scaling-stroke"
      />
    </svg>
  );
}

const DEFAULT_SERIES = [42, 44, 40, 48, 52, 49, 55, 58, 54, 61, 59, 64];

export function MetricTile({
  label,
  value,
  unit,
  delta,
  deltaGoodDirection = "up",
  series = DEFAULT_SERIES,
  threshold = "ok",
  className,
  ...props
}: MetricTileProps) {
  const color = THRESHOLD_VAR[threshold];
  const hasDelta = typeof delta === "number";
  const rising = hasDelta && delta! >= 0;
  const good = hasDelta && (rising ? deltaGoodDirection === "up" : deltaGoodDirection === "down");
  const deltaColor = good ? "var(--color-success)" : "var(--color-destructive)";

  return (
    <div
      className={cn(
        "flex w-full max-w-[220px] flex-col gap-2 rounded-xl bg-card p-4 text-card-foreground shadow-border",
        className,
      )}
      {...props}
    >
      <div className="flex items-center gap-2">
        <span
          aria-hidden
          className="size-1.5 rounded-full"
          style={{ background: color }}
        />
        <span className="truncate text-xs font-medium text-muted-foreground">
          {label}
        </span>
      </div>
      <div className="flex items-baseline gap-1">
        <span className="text-3xl font-semibold tabular-nums leading-none tracking-tight">
          {value}
        </span>
        {unit && (
          <span className="text-sm font-medium text-muted-foreground">{unit}</span>
        )}
      </div>
      {hasDelta && (
        <div
          className="flex items-center gap-0.5 text-xs font-medium tabular-nums"
          style={{ color: deltaColor }}
        >
          {rising ? (
            <ArrowUpRight className="size-3.5" />
          ) : (
            <ArrowDownRight className="size-3.5" />
          )}
          {Math.abs(delta!).toFixed(1)}%
          <span className="ml-1 font-normal text-muted-foreground">vs 24h</span>
        </div>
      )}
      <Sparkline series={series} color={color} />
    </div>
  );
}