KPI Tile

Dashboard metric card. Surfaces a single number with context — icon well, trend delta, optional progress bar, and semantic variant colouring. Composes Card, Badge, and Progress Bar primitives.

Preview

12%

2,847

of 3,000 target

Active Users

8%

$48.2K

this month

Revenue

5%

23

4 critical

Open Issues

2%

0.4%

last 24h

Error Rate

Anatomy

SlotElementRequired
Icon well44×44 rounded rect, accent-tinted background, specular top edgeNo
Trend badgeCapsule with directional arrow + delta %, semantic colourNo
Value22 px bold monospaced digit, tabular-nums variantYes
Subtitle11 px tertiary — goal context, secondary metricNo
Label12 px medium secondary — metric nameYes
Progress bar4 px mini progress bar anchored to card bottomNo

Implementation

KPITile.tsx
tsx
import { cn } from "@/lib";
import { TrendingUp, TrendingDown, Minus } from "lucide-react";

interface KPITileTrend {
  value: number;
  label?: string;
  direction: "up" | "down" | "neutral";
}

interface KPITileProps {
  title: string;
  value: string | number;
  icon?: React.ReactNode;
  trend?: KPITileTrend;
  variant?: "default" | "accent" | "success" | "warning" | "danger";
  progress?: number;
  subtitle?: string;
  className?: string;
}

const variantConfig = {
  default:  { well: "bg-[rgb(var(--accent-subtle))]",        icon: "text-[rgb(var(--accent))]" },
  accent:   { well: "bg-[rgb(var(--accent-subtle))]",        icon: "text-[rgb(var(--accent))]" },
  success:  { well: "bg-[rgb(var(--status-success)/0.12)]",  icon: "text-[rgb(var(--status-success))]" },
  warning:  { well: "bg-[rgb(var(--status-warning)/0.12)]",  icon: "text-[rgb(var(--status-warning))]" },
  danger:   { well: "bg-[rgb(var(--status-danger)/0.12)]",   icon: "text-[rgb(var(--status-danger))]" },
};

const progressColors = {
  default: "rgb(var(--accent))",
  accent:  "rgb(var(--accent))",
  success: "rgb(var(--status-success))",
  warning: "rgb(var(--status-warning))",
  danger:  "rgb(var(--status-danger))",
};

export function KPITile({
  title, value, icon, trend, variant = "default",
  progress, subtitle, className,
}: KPITileProps) {
  const cfg = variantConfig[variant];

  return (
    <div
      className={cn(
        "rounded-[var(--radius-lg)] border border-[rgb(var(--border))] bg-[rgb(var(--surface))]",
        "p-4 flex flex-col gap-3",
        "shadow-[var(--shadow-card)]",
        className
      )}
    >
      <div className="flex items-start justify-between gap-3">
        {icon && (
          <div className={cn("w-11 h-11 rounded-[var(--radius-md)] flex items-center justify-center shrink-0 relative overflow-hidden", cfg.well)}>
            {/* Specular top-edge highlight */}
            <div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-white/20 to-transparent pointer-events-none" />
            <span className={cn("w-5 h-5", cfg.icon)}>{icon}</span>
          </div>
        )}
        {trend && (
          <div className={cn(
            "flex items-center gap-1 text-[11px] font-medium rounded-full px-2 py-0.5 ml-auto shrink-0",
            trend.direction === "up"      && "bg-[rgb(var(--status-success)/0.12)] text-[rgb(var(--status-success))]",
            trend.direction === "down"    && "bg-[rgb(var(--status-danger)/0.12)]  text-[rgb(var(--status-danger))]",
            trend.direction === "neutral" && "bg-[rgb(var(--surface-raised))]      text-[rgb(var(--text-tertiary))]",
          )}>
            {trend.direction === "up"      && <TrendingUp className="w-3 h-3" />}
            {trend.direction === "down"    && <TrendingDown className="w-3 h-3" />}
            {trend.direction === "neutral" && <Minus className="w-3 h-3" />}
            {Math.abs(trend.value)}%{trend.label && <span className="ml-0.5 opacity-70">{trend.label}</span>}
          </div>
        )}
      </div>

      <div>
        <p className="text-[22px] font-bold leading-none tracking-tight text-[rgb(var(--text-primary))] font-[var(--font-mono)]">
          {value}
        </p>
        {subtitle && (
          <p className="text-[11px] text-[rgb(var(--text-tertiary))] mt-1">{subtitle}</p>
        )}
      </div>

      <p className="text-[12px] font-medium text-[rgb(var(--text-secondary))]">{title}</p>

      {progress !== undefined && (
        <div className="relative h-1 rounded-full overflow-hidden" style={{ background: "rgb(var(--progress-track))" }}>
          <div className="absolute inset-y-0 left-0 rounded-full" style={{ width: `${Math.min(100, progress)}%`, background: progressColors[variant] }} />
        </div>
      )}
    </div>
  );
}

Props

PropTypeDefaultDescription
titlestringMetric label displayed below the value.
valuestring | numberPrimary metric value. Pass formatted strings for currency or large numbers.
iconReactNodeIcon rendered in the 44×44 accent-tinted icon well.
trend{ value: number; label?: string; direction: "up" | "down" | "neutral" }Optional delta indicator. Positive direction = green arrow, negative = red.
variant"default" | "accent" | "success" | "warning" | "danger""default"Tints the icon well and activates the brand-lit surface on accent variant.
progressnumber0–100. When set, renders a mini progress bar beneath the value.
subtitlestringSecondary line below value — useful for goal context (e.g. 'of 50 target').

Accessibility

  • KPI tiles are non-interactive — they are not focusable and do not receive keyboard events.
  • Trend direction is communicated visually and via the label text; do not rely solely on the arrow icon.
  • For live-updating metrics, wrap the tile in an aria-live='polite' region so screen readers announce changes.
  • Icon wells use aria-hidden='true' — the metric value and label carry the accessible meaning.