Summary / KPI Table
A compact metrics table — current value, period-over-period change, and a per-row sparkline — for the kind of dashboard summary that sits above the fold.
npx shadcn add https://shadcn-table-library.vercel.app/r/kpi-table.json| Metric | Value | Change | Trend |
|---|---|---|---|
| Revenue | $128,400 | +8.2% | |
| New Customers | 482 | -3.4% | |
| Churn Rate | 3.1% | -0.4% | |
| Conversion Rate | 4.6% | +0.3% | |
| Avg Order Value | $64 | +1.1% |
How it works
1.The sparkline is a hand-rolled SVG, not a charting library
Sparkline normalizes a small array of numbers to a 0..height range and joins the points into a single <polyline points="...">. For 8-12 points that's simpler and lighter than pulling in a charting dependency to draw what is, in the end, just one line.
const min = Math.min(...data)const max = Math.max(...data)const range = max - min || 1const stepX = width / (data.length - 1)const points = data.map((value, i) => {const x = i * stepXconst y = height - ((value - min) / range) * heightreturn `${x.toFixed(1)},${y.toFixed(1)}`}).join(' ')
2.Color comes from one number, reused twice
change's sign drives both the TrendingUp/TrendingDown icon and color in the Change column, and the Sparkline's text-emerald/text-rose class in the Trend column. The badge and the sparkline can never disagree about whether a metric is trending up or down — they're colored by the exact same comparison, not two separate ones.
<Sparklinedata={row.original.trend}className={cn('h-7 w-24',row.original.change >= 0? 'text-emerald-600 dark:text-emerald-500': 'text-rose-600 dark:text-rose-500',)}/>
3.The stroke color comes from currentColor, not a prop
Sparkline's polyline sets stroke="currentColor" and takes its actual color from a Tailwind text-color class on the wrapping element — the same trick lucide-react icons use elsewhere in this library. That's how light/dark mode work for the sparkline for free, without the component itself knowing anything about the app's theme.
<polylinepoints={points}fill="none"stroke="currentColor"strokeWidth={1.5}strokeLinecap="round"strokeLinejoin="round"/>
4.No sorting, filtering, or pagination — this is a dashboard widget
With a small, fixed set of KPI rows meant to all be visible at once, getSortedRowModel/getFilteredRowModel/getPaginationRowModel would just be complexity a summary card never needs. getCoreRowModel is the entire row-model pipeline here.
const table = useReactTable({data,columns,getCoreRowModel: getCoreRowModel(),})
5.Formatting is unit-aware, driven by each KPI's own unit field
formatKpiValue branches on unit ('currency' | 'percent' | 'number'), so Revenue renders as $128,400, Churn Rate renders as 3.1%, and New Customers renders as a plain 482 — all from one shared formatting function instead of ad hoc .toFixed() calls scattered across column definitions.
export function formatKpiValue(value: number, unit: KpiUnit): string {if (unit === 'currency') {return value.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 })}if (unit === 'percent') return `${value.toFixed(1)}%`return value.toLocaleString('en-US')}