Skip to content
ilinxa/pro-ui

Stat Card

alphav0.1.2

Single-metric dashboard card — value, label, delta, and a dependency-free SVG sparkline with polarity-aware coloring.

Category: Data DisplayUpdated: 2026-08-11Created: 2026-05-09Author: ilinxa

Context

Use anywhere a single calibrated metric needs to surface — admin / ops dashboards, product analytics surfaces, financial dashboards, observability views, marketing metrics. Object-shape `delta` from day one (F-cross-12-correct). Default delta format = locale-aware `Intl.NumberFormat` percent (`0.124` → `+12.4%`); `betterIsHigher: false` flips the green/red semantics for cost / error metrics where ↑ is bad. Sibling `<StatCardSparkline>` export covers standalone sparkline use without the card chrome. First component in the metrics-domain family; future siblings (TBD): kpi-grid (responsive layout), gauge-card (radial), comparison-card (two-up).

Installation

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Install the component
pnpm dlx shadcn@latest add @ilinxa/stat-card

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/stat-card-fixtures

CLI can't resolve @ilinxa? The namespace is listed in the official shadcn registry directory, so current CLIs need no configuration. If yours can't resolve it (older or pinned versions, self-hosted mirrors), register it manually in components.json:

"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}

Preview

Revenue this month
$12,431
+12.4%increasevs last period
Error rate (last 24h)
42.00%
+8%increasevs last period
Active users
1,234
Average response time
42.7 ms
-8%decreasevs last period

Default variant — 4-card strip. Revenue defaults to betterIsHigher: true (↑ green); error rate sets betterIsHigher: false (↑ red); latency went down with betterIsHigher: false (↓ green).

Demo source

demo.tsxtsx
"use client"; import { useState } from "react";import {  AlertTriangle,  Clock,  DollarSign,  ShieldCheck,  Users,} from "lucide-react";import { Button } from "@/components/ui/button";import { Tabs, TabsContent, TabsTrigger } from "@/components/ui/tabs";import { SwipeTabsList } from "@/components/site/swipe-tabs-list";import { StatCard } from "./stat-card";import { StatCardSparkline } from "./parts/sparkline";import {  STAT_CARD_DUMMY_ACTIVE_USERS,  STAT_CARD_DUMMY_ERROR_RATE,  STAT_CARD_DUMMY_LATENCY,  STAT_CARD_DUMMY_REVENUE,  STAT_CARD_DUMMY_SIGNUPS,  STAT_CARD_DUMMY_UPTIME,} from "./dummy-data"; export default function StatCardDemo() {  const [loading, setLoading] = useState(false);   return (    <Tabs defaultValue="default" className="w-full">      <SwipeTabsList>        <TabsTrigger value="default">Default</TabsTrigger>        <TabsTrigger value="compact">Compact</TabsTrigger>        <TabsTrigger value="detailed">Detailed</TabsTrigger>        <TabsTrigger value="loading">Loading</TabsTrigger>        <TabsTrigger value="empty">Empty</TabsTrigger>        <TabsTrigger value="matrix">Color logic matrix</TabsTrigger>        <TabsTrigger value="custom-value">Custom value</TabsTrigger>        <TabsTrigger value="sparkline-only">Sparkline only</TabsTrigger>        <TabsTrigger value="i18n">Localized (TR)</TabsTrigger>      </SwipeTabsList>       {/* Default — 4-card KPI strip */}      <TabsContent value="default" className="mt-6">        <div className="grid grid-cols-2 gap-4 lg:grid-cols-4">          <StatCard            {...STAT_CARD_DUMMY_REVENUE}            icon={DollarSign}            formatValue={(v) =>              typeof v === "number"                ? `$${v.toLocaleString()}`                : String(v)            }          />          <StatCard            {...STAT_CARD_DUMMY_ERROR_RATE}            icon={AlertTriangle}            formatValue={(v) =>              typeof v === "number" ? `${(v * 100).toFixed(2)}%` : String(v)            }          />          <StatCard            {...STAT_CARD_DUMMY_ACTIVE_USERS}            icon={Users}            formatValue={(v) => v.toLocaleString()}          />          <StatCard            {...STAT_CARD_DUMMY_LATENCY}            icon={Clock}            formatValue={(v) =>              typeof v === "number" ? `${v.toFixed(1)} ms` : String(v)            }          />        </div>        <p className="mt-4 text-xs text-muted-foreground">          Default variant — 4-card strip. Revenue defaults to{" "}          <code>betterIsHigher: true</code> (↑ green); error rate sets{" "}          <code>betterIsHigher: false</code> (↑ red); latency went down with{" "}          <code>betterIsHigher: false</code> (↓ green).        </p>      </TabsContent>       {/* Compact — sidebar widgets */}      <TabsContent value="compact" className="mt-6">        <div className="grid max-w-sm gap-3">          <StatCard            value={42}            label="Posts this week"            variant="compact"            delta={{ value: 0.18 }}          />          <StatCard            value={"12.4k"}            label="Reach"            variant="compact"            delta={{ value: 0.04 }}          />          <StatCard            value={0.067}            label="Engagement rate"            variant="compact"            formatValue={(v) =>              typeof v === "number" ? `${(v * 100).toFixed(1)}%` : String(v)            }            delta={{ value: -0.012 }}          />        </div>        <p className="mt-4 text-xs text-muted-foreground">          Compact variant — sidebar / dense KPI grid. No sparkline; tighter          vertical rhythm.        </p>      </TabsContent>       {/* Detailed — hero KPI cards */}      <TabsContent value="detailed" className="mt-6">        <div className="grid gap-4 md:grid-cols-2">          <StatCard            {...STAT_CARD_DUMMY_REVENUE}            variant="detailed"            icon={DollarSign}            formatValue={(v) =>              typeof v === "number" ? `$${v.toLocaleString()}` : String(v)            }            delta={{ value: 0.124, period: "vs last 30 days" }}            href="#"            ariaLabel="Revenue this month — open detail"          />          <StatCard            {...STAT_CARD_DUMMY_UPTIME}            variant="detailed"            icon={ShieldCheck}            formatValue={(v) =>              typeof v === "number" ? `${(v * 100).toFixed(2)}%` : String(v)            }          />        </div>        <p className="mt-4 text-xs text-muted-foreground">          Detailed variant — hero KPI cards. Larger value, mandatory          sparkline, larger icon. Revenue card is linked (focus-ring + cursor          pointer when keyboarded).        </p>      </TabsContent>       {/* Loading state */}      <TabsContent value="loading" className="mt-6">        <div className="mb-4">          <Button onClick={() => setLoading((l) => !l)} size="sm">            {loading ? "Stop loading" : "Start loading"}          </Button>        </div>        <div className="grid grid-cols-2 gap-4 lg:grid-cols-4">          <StatCard            {...STAT_CARD_DUMMY_REVENUE}            icon={DollarSign}            loading={loading}            formatValue={(v) =>              typeof v === "number" ? `$${v.toLocaleString()}` : String(v)            }          />          <StatCard            {...STAT_CARD_DUMMY_ERROR_RATE}            icon={AlertTriangle}            loading={loading}          />          <StatCard            {...STAT_CARD_DUMMY_ACTIVE_USERS}            icon={Users}            loading={loading}          />          <StatCard            {...STAT_CARD_DUMMY_LATENCY}            icon={Clock}            loading={loading}          />        </div>        <p className="mt-4 text-xs text-muted-foreground">          Toggle to flip <code>loading</code> on each card. Skeleton matches          the loaded shape — no layout shift on hydration.        </p>      </TabsContent>       {/* Empty state */}      <TabsContent value="empty" className="mt-6">        <div className="grid grid-cols-2 gap-4 lg:grid-cols-4">          <StatCard            value={undefined}            label="Awaiting data"            icon={Users}          />          <StatCard            value={undefined}            label="No baseline yet"            icon={Clock}            labels={{ emptyValueLabel: "No data" }}          />          <StatCard value={undefined} label="Pending" variant="compact" />          <StatCard            value={undefined}            label="Pending hero"            variant="detailed"            icon={ShieldCheck}          />        </div>        <p className="mt-4 text-xs text-muted-foreground">          When <code>value === undefined</code>, the card renders{" "}          <code>labels.emptyValueLabel</code> (default <code>—</code>) at the          value position with muted styling — same height as a real value, no          layout jump.        </p>      </TabsContent>       {/* Color logic matrix — 5 cells */}      <TabsContent value="matrix" className="mt-6">        <div className="grid grid-cols-1 gap-4 md:grid-cols-3">          <StatCard            value={120}            label="positive × betterIsHigher: true"            variant="compact"            delta={{ value: 0.124 }}          />          <StatCard            value={120}            label="positive × betterIsHigher: false"            variant="compact"            delta={{ value: 0.124, betterIsHigher: false }}          />          <StatCard            value={120}            label="negative × betterIsHigher: true"            variant="compact"            delta={{ value: -0.124 }}          />          <StatCard            value={120}            label="negative × betterIsHigher: false"            variant="compact"            delta={{ value: -0.124, betterIsHigher: false }}          />          <StatCard            value={120}            label="zero (any betterIsHigher)"            variant="compact"            delta={{ value: 0 }}          />        </div>        <p className="mt-4 text-xs text-muted-foreground">          5 visually-distinct combinations. Zero delta renders neutral          regardless of <code>betterIsHigher</code> (the 6th &quot;cell&quot;          would be visually identical, so it&apos;s elided here).        </p>      </TabsContent>       {/* Custom value — unit superscript pattern */}      <TabsContent value="custom-value" className="mt-6">        <div className="grid max-w-md gap-4">          <StatCard            value={42.7}            label="Average response time"            icon={Clock}            renderValue={({ value }) => (              <span>                {typeof value === "number" ? value.toFixed(1) : value}                <span className="ml-1 text-base text-muted-foreground">ms</span>              </span>            )}            delta={{ value: -0.08, betterIsHigher: false }}          />          <StatCard            {...STAT_CARD_DUMMY_SIGNUPS}            icon={Users}            formatValue={(v) =>              typeof v === "number" ? v.toLocaleString() : String(v)            }          />        </div>        <p className="mt-4 text-xs text-muted-foreground">          <code>renderValue</code> takes over the value cell entirely (used          here for the unit-superscript &quot;ms&quot; treatment). Signup          card uses <code>delta.format</code> override for absolute-count          delta — &quot;+1,240&quot; instead of the default &quot;+1240%&quot;.        </p>      </TabsContent>       {/* Standalone sparkline */}      <TabsContent value="sparkline-only" className="mt-6">        <div className="grid max-w-md gap-4">          <div className="flex items-center justify-between rounded-lg border border-border bg-card p-4">            <span className="text-sm font-medium">CPU utilization</span>            <StatCardSparkline              data={[42, 48, 55, 62, 68, 71, 67, 64]}              className="h-8 w-32 text-primary"            />          </div>          <div className="flex items-center justify-between rounded-lg border border-border bg-card p-4">            <span className="text-sm font-medium">Memory pressure</span>            <StatCardSparkline              data={[0.82, 0.85, 0.88, 0.84, 0.81, 0.79, 0.83, 0.86]}              className="h-8 w-32 text-destructive"            />          </div>        </div>        <p className="mt-4 text-xs text-muted-foreground">          <code>&lt;StatCardSparkline&gt;</code> is a sibling export — usable          standalone, no card chrome. <code>className</code> drives size +          color (text-color inheritance via <code>currentColor</code>).        </p>      </TabsContent>       {/* i18n */}      <TabsContent value="i18n" className="mt-6">        <div className="grid max-w-md gap-4">          <StatCard            value={12431}            label="Bu ay gelir"            icon={DollarSign}            formatValue={(v) =>              typeof v === "number" ? `₺${v.toLocaleString("tr-TR")}` : String(v)            }            delta={{              value: 0.124,              format: (v) =>                v.toLocaleString("tr-TR", {                  style: "percent",                  signDisplay: "exceptZero",                  maximumFractionDigits: 1,                }),              period: "geçen aya kıyasla",            }}            labels={{              deltaPrefix: "vs.",              deltaPeriod: "geçen dönem",              increaseLabel: "artış",              decreaseLabel: "azalış",              loadingLabel: "Veri yükleniyor…",              emptyValueLabel: "—",            }}          />        </div>        <p className="mt-4 text-xs text-muted-foreground">          All consumer-visible strings overridable via <code>labels</code>.          Number formatting via <code>delta.format</code> + locale arg          (here <code>tr-TR</code>).        </p>      </TabsContent>    </Tabs>  );} 

Usage

Quick start

Single-metric dashboard widget. Required props are value and label. Add delta for vs-prior-period change, trend for a sparkline, icon for a leading visual marker, variant to change density.

import { StatCard } from "@/components/stat-card"
import { DollarSign } from "lucide-react"

export function RevenueCard() {
  return (
    <StatCard
      value={12431}
      label="Revenue this month"
      icon={DollarSign}
      formatValue={(v) => `$${v.toLocaleString()}`}
      delta={{ value: 0.124 }}
      trend={[8200, 8800, 9100, 10200, 10900, 11500, 12100, 12431]}
    />
  )
}

Variants

  • default — value + label + delta + sparkline. The canonical KPI strip card.
  • compact — smaller value, no sparkline. Sidebar widgets and dense KPI grids.
  • detailed — larger value + mandatory sparkline + larger icon. Hero KPI cards.

Delta polarity (`betterIsHigher`)

Default: betterIsHigher: true — a positive delta colors green, a negative delta colors red. Set false for cost / error metrics where ↑ is bad. The default delta.format is locale-aware Intl.NumberFormat percent — delta.value is a fraction (0.124 = +12.4%).

<StatCard
  value={errorRate}
  label="Error rate (last 24h)"
  delta={{ value: 0.08, betterIsHigher: false }}  // up = bad → red
/>

<StatCard
  value={signupCount}
  label="New signups this week"
  delta={{
    value: 1240,
    format: (v) => v.toLocaleString(undefined, { signDisplay: "exceptZero" }),
  }}
/>

Custom value rendering

Use renderValue for unit superscripts, tooltips, or any composite value treatment. Receives { value, loading }.

<StatCard
  value={42.7}
  label="Average response time"
  renderValue={({ value }) => (
    <span>
      {value.toFixed(1)}
      <span className="text-muted-foreground text-base ml-1">ms</span>
    </span>
  )}
/>

Sparkline

Pass trend as a flat number[]; the component renders a built-in pure-SVG sparkline (no charting peer dep). Cap at 100 points; larger arrays are uniformly downsampled. For non-line shapes (bars, dual-axis) use the renderTrend slot. The StatCardSparkline sub-component is exported separately for standalone use:

import { StatCardSparkline } from "@/components/stat-card"

<StatCardSparkline
  data={[42, 48, 55, 62, 68, 71, 67, 64]}
  className="h-8 w-32 text-primary"
/>

Loading + empty

loading=true renders a shape-matched skeleton — aria-busy+ sr-only loading announcement. Skeleton size matches the loaded variant so there's no layout shift on hydration. value=undefined renders labels.emptyValueLabel (default —) at the value position; same height, muted color.

i18n

Labels are overridable via the labels prop. Defaults are English; override deltaPrefix, deltaPeriod, loadingLabel, increaseLabel, decreaseLabel, emptyValueLabel. Number formatting via delta.format with a locale arg.

Accessibility

  • Root is <dl> — screen readers announce label-value pairs natively.
  • Sparkline + arrow are aria-hidden; the delta has an sr-only increaseLabel /decreaseLabelso SRs say "12.4% increase" not "12.4% up arrow".
  • Linked variant uses overlay-link pattern: focus-visible ring on the card; the link element is transparent on top.
  • Loading state is aria-busy="true" with sr-only loading announcement.

Features

  • 3 variants — default / compact / detailed
  • Object-shape delta callback (F-cross-12-correct from day one)
  • betterIsHigher boolean for cost / error semantics (default true)
  • Built-in pure-SVG sparkline (no charting peer dep, ~50 LOC)
  • Up to 100 trend points (uniform downsampling for larger datasets)
  • <StatCardSparkline> sibling export for standalone use
  • Default Intl.NumberFormat percent formatter (locale-aware, signDisplay: exceptZero)
  • Loading state with shape-matched skeleton
  • Empty state ('—' at value position when value is undefined)
  • <dl> root for native screen-reader label-value pairs
  • Polymorphic root via linkComponent + href (overlay-link pattern)
  • renderValue + renderTrend escape hatches (object-shape contexts)
  • WCAG 2.1 AA — sr-only delta semantic, aria-hidden sparkline + arrow, focus-visible ring on link

Tags

stat-cardmetrickpidashboardsparklinedelta

Dependencies

shadcn primitives: skeleton