# KPI Card Design Patterns
## Table of Contents
- [Essential Elements](#essential-elements)
- [Number Formatting](#number-formatting)
- [Trend Indicators](#trend-indicators)
- [Sparkline Integration](#sparkline-integration)
- [Card Variations](#card-variations)
- [Interactive Features](#interactive-features)
- [Accessibility](#accessibility)
## Essential Elements
### Basic KPI Card Structure
```tsx
interface KPICard {
label: string; // "Monthly Revenue"
value: number; // 1245832
unit?: string; // "$", "%", "users"
trend?: {
direction: 'up' | 'down' | 'neutral';
value: number; // 15.3
comparison: string; // "vs last month"
};
sparkline?: number[]; // [100, 120, 115, 140, 155]
status?: 'success' | 'warning' | 'error' | 'neutral';
icon?: ReactNode;
onClick?: () => void;
}
```
### Visual Hierarchy
```
┌──────────────────────────────┐
│ 👥 Active Users [ℹ️] [⚙️] │ ← Icon, Label, Actions
│ │
│ 12,543 │ ← Primary Value (largest)
│ │
│ ↑ 234 (+1.9%) │ ← Change (medium)
│ vs yesterday │ ← Comparison (small)
│ │
│ ▂▃▅▄▆▇█▆▅ Last 7 days │ ← Sparkline + Period
└──────────────────────────────┘
```
## Number Formatting
### Large Number Abbreviation
```typescript
function formatLargeNumber(value: number): string {
const abbreviations = [
{ threshold: 1e9, suffix: 'B' },
{ threshold: 1e6, suffix: 'M' },
{ threshold: 1e3, suffix: 'K' }
];
for (const { threshold, suffix } of abbreviations) {
if (Math.abs(value) >= threshold) {
const formatted = (value / threshold).toFixed(1);
return `${formatted}${suffix}`;
}
}
return value.toLocaleString();
}
// Examples:
// 1234567 → "1.2M"
// 45678 → "45.7K"
// 999 → "999"
```
### Currency Formatting
```typescript
function formatCurrency(value: number, currency = 'USD'): string {
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
minimumFractionDigits: 0,
maximumFractionDigits: 0,
notation: value > 1e6 ? 'compact' : 'standard'
});
return formatter.format(value);
}
// Examples:
// 1234567 → "$1.2M"
// 45678 → "$45,678"
// 999.99 → "$1,000"
```
### Percentage Formatting
```typescript
function formatPercentage(value: number, decimals = 1): string {
const formatted = value.toFixed(decimals);
const sign = value > 0 ? '+' : '';
return `${sign}${formatted}%`;
}
// Examples:
// 15.345 → "+15.3%"
// -2.1 → "-2.1%"
// 0 → "0.0%"
```
## Trend Indicators
### Direction Arrows
```tsx
function TrendArrow({ direction, value }: TrendProps) {
const arrows = {
up: '↑',
down: '↓',
neutral: '→'
};
const colors = {
up: 'text-green-600',
down: 'text-red-600',
neutral: 'text-gray-500'
};
return (
{arrows[direction]}
{formatPercentage(value)}
);
}
```
### Status Badges
```tsx
function StatusBadge({ deltaType, value, label }: BadgeProps) {
const styles = {
increase: 'bg-green-100 text-green-800',
decrease: 'bg-red-100 text-red-800',
neutral: 'bg-gray-100 text-gray-800'
};
return (
{value}
{label && {label}}
);
}
```
## Sparkline Integration
### Basic Sparkline
```tsx
import { Sparklines, SparklinesLine, SparklinesSpots } from 'react-sparklines';
function KPISparkline({ data, color = '#10b981' }) {
return (
);
}
```
### Inline SVG Sparkline (No Library)
```tsx
function MiniSparkline({ data, width = 100, height = 30 }) {
const max = Math.max(...data);
const min = Math.min(...data);
const range = max - min || 1;
const points = data.map((value, index) => {
const x = (index / (data.length - 1)) * width;
const y = height - ((value - min) / range) * height;
return `${x},${y}`;
}).join(' ');
return (
);
}
```
## Card Variations
### Compact KPI Card
```tsx
function CompactKPI({ label, value, trend }) {
return (
);
}
```
### Detailed KPI Card
```tsx
function DetailedKPI({
label,
value,
trend,
sparkline,
breakdown,
actions
}) {
return (
{/* Header */}
{label}
{actions?.map(action => (
))}
{/* Main Value */}
{/* Sparkline */}
{sparkline && (
)}
{/* Breakdown */}
{breakdown && (
{breakdown.map(item => (
{item.label}
{item.value}
))}
)}
);
}
```
### Goal-Based KPI Card
```tsx
function GoalKPI({ label, current, target, unit = '' }) {
const percentage = (current / target) * 100;
const status = percentage >= 100 ? 'success' :
percentage >= 75 ? 'warning' : 'error';
return (
{label}
{unit}{current.toLocaleString()}
of {unit}{target.toLocaleString()}
{percentage.toFixed(1)}% of goal achieved
);
}
```
## Interactive Features
### Clickable KPI with Drill-Down
```tsx
function InteractiveKPI({ data, onDrillDown }) {
const [isHovered, setIsHovered] = useState(false);
return (
setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={() => onDrillDown(data.id)}
style={{
boxShadow: isHovered ? 'var(--shadow-lg)' : 'var(--shadow-md)'
}}
>
{data.label}
{data.value}
{isHovered && (
)}
{data.trend && (
)}
);
}
```
### Real-Time Updating KPI
```tsx
function LiveKPI({ endpoint, label }) {
const [data, setData] = useState(null);
const [isUpdating, setIsUpdating] = useState(false);
useEffect(() => {
const eventSource = new EventSource(endpoint);
eventSource.onmessage = (event) => {
setIsUpdating(true);
const newData = JSON.parse(event.data);
setTimeout(() => {
setData(newData);
setIsUpdating(false);
}, 300);
};
return () => eventSource.close();
}, [endpoint]);
return (
{label}
{data?.value || '---'}
{data?.lastUpdated && (
Updated {formatRelativeTime(data.lastUpdated)}
)}
);
}
```
## Accessibility
### ARIA Labels and Roles
```tsx
function AccessibleKPI({ label, value, trend }) {
const trendDescription = trend
? `${trend.direction} ${trend.value}% ${trend.comparison}`
: '';
return (
{label}
Current value:
{value}
{trend && (
Trend:
)}
);
}
```
### Keyboard Navigation
```tsx
function KeyboardNavigableKPI({ data, onSelect }) {
return (
{
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onSelect(data);
}
}}
onClick={() => onSelect(data)}
className="kpi-card focus:ring-2 focus:ring-blue-500"
>
{/* KPI content */}
);
}
```
## Complete KPI Card Implementation
```tsx
function KPICard({
label,
value,
unit = '',
trend,
sparkline,
status = 'neutral',
icon,
onClick,
className = ''
}) {
const formattedValue = typeof value === 'number'
? formatLargeNumber(value)
: value;
const statusColors = {
success: 'border-l-4 border-green-500',
warning: 'border-l-4 border-yellow-500',
error: 'border-l-4 border-red-500',
neutral: ''
};
return (
{/* Header */}
{/* Value */}
{unit && {unit}}
{formattedValue}
{/* Trend */}
{trend && (
{formatPercentage(trend.value)}
{trend.comparison}
)}
{/* Sparkline */}
{sparkline && sparkline.length > 0 && (
)}
);
}
```