-
Notifications
You must be signed in to change notification settings - Fork 40
KPI Card Variations
A 2x2 KPI grid is a staple of dashboard design. This guide shows how to make the 4 cards feel distinct and purposeful rather than repetitive.
<StatCard
icon={CreditCard}
label="TODAY'S REVENUE"
value="1,870"
unit="K"
trend={{ value: "+8.2%", direction: "up" }}
/>Internal structure:
┌──────────────────────────┐
│ [icon badge] LABEL │ ← 12px uppercase tracking-wide
│ │
│ 1,870K │ ← 36px bold + 18px unit (2:1)
│ │
│ +8.2% ↑ │ ← 13px bold, text-success
└──────────────────────────┘
Each card should have a unique, semantically meaningful icon. This breaks the visual silhouette:
| Metric | Icon | Why |
|---|---|---|
| Revenue |
Wallet or DollarSign
|
Money container |
| Orders | Package |
Physical goods |
| Visitors | Users |
People |
| Conversion | Target |
Goal/aim |
| Churn | UserMinus |
User leaving |
| Growth | TrendingUp |
Direction |
Different units create different text rhythm within the 2:1 ratio:
{/* Short unit -- feels compact */}
<StatCard value="3.8" unit="M" />
{/* Medium unit -- balanced */}
<StatCard value="1,870" unit="USD" />
{/* Percentage -- minimal */}
<StatCard value="12.4" unit="%" />
{/* No unit -- just a count */}
<StatCard value="247" />When all 4 trends point up, the grid looks artificially positive. Real data naturally mixes:
<div className="grid grid-cols-2 gap-4 px-6">
<StatCard label="REVENUE" trend={{ value: "+14.2%", direction: "up" }} ... />
<StatCard label="CHURN RATE" trend={{ value: "-0.4%", direction: "down" }} ... />
<StatCard label="ARPU" trend={{ value: "+1.8%", direction: "up" }} ... />
<StatCard label="SUPPORT" trend={{ value: "+12", direction: "up" }} ... />
</div>A "down" trend uses text-destructive and TrendingDown, naturally creating color contrast.
Different scales of numbers prevent the grid from feeling uniform:
| Card | Value | Visual Texture |
|---|---|---|
| Card 1 | 3.8M |
Short, with decimal |
| Card 2 | 1,870 |
Medium, with comma |
| Card 3 | 247 |
Short integer |
| Card 4 | 12.4% |
Decimal percentage |
One card in the grid can include a progress bar below the metric, making it visually taller and heavier:
<div className="bg-card rounded-2xl p-6 shadow-[var(--shadow-card)]">
<div className="flex items-center gap-2 mb-3">
<div className="size-7 rounded-lg bg-brand/10 flex items-center justify-center">
<Target className="size-4 text-brand" />
</div>
<p className="text-[12px] text-text-secondary font-medium uppercase tracking-[0.05em]">
GOAL PROGRESS
</p>
</div>
<p className="text-text-primary text-[36px] font-bold leading-none whitespace-nowrap">
68<span className="text-[18px] ms-0.5">%</span>
</p>
{/* Progress bar adds visual weight */}
<div className="mt-3 bg-surface-muted rounded-full h-4 overflow-hidden">
<div className="bg-brand h-full w-[68%] rounded-full" />
</div>
</div>For discrete metrics (8 out of 10 tasks done), use the segment bar instead of continuous progress:
<div className="mt-3 flex gap-1">
{[...Array(10)].map((_, i) => (
<div key={i} className={`h-6 flex-1 rounded ${i < 8 ? 'bg-brand' : 'bg-surface-muted'}`} />
))}
</div><div className="grid grid-cols-2 gap-4 px-6">
{/* 4 cards */}
</div><div className="space-y-4 px-6">
{/* Full-width stat card with progress bar */}
<StatCard className="col-span-2" ... />
<div className="grid grid-cols-2 gap-4">
<StatCard ... />
<StatCard ... />
</div>
</div>For information-dense dashboards with 6 mini metrics:
<div className="grid grid-cols-3 gap-3 px-6">
{/* 3 or 6 compact cards */}
</div>| Element | Color | Rule |
|---|---|---|
| Icon badge background | bg-brand/10 |
Always 10% opacity of accent |
| Icon | text-brand |
Always accent color |
| Value text |
text-text-primary (#3C3C3C) |
Never accent color |
| Up trend |
text-success (#6B9B7A) |
Calm green, not vivid |
| Down trend |
text-destructive (#D4183D) |
Only for negative values |
| Label |
text-text-secondary (#6A6A6A) |
Always secondary gray |
| Unit |
text-text-primary at smaller size |
Same color as value, smaller |
| Mistake | Fix |
|---|---|
| All 4 cards use the same icon | Use semantically distinct icons |
| Trend shows "+0.0%" | Hide trend icon when value is zero; show "0%" in gray |
| Giant numbers overflow the card | Scale unit up: 18,700,000 -> 1,870K
|
| Adding borders to individual grid cards | Cards use shadow only, no borders (light mode) |
| Making one card a different background color | All cards are bg-card (white), no exceptions |