A hero metric card with a large tabular-numerals value, a trend pill that flips to red on negative deltas, a comparison label, and an inline SVG sparkline whose stroke and fill follow the trend direction. Inspired by Stripe.
Revenue this month
$12,453.20
Tap a metric to switch · Trend pill turns red on negative · Sparkline color follows direction
// Dependencies: react ^18, lucide-react
import React, { useState } from 'react';
import { TrendingUp, TrendingDown } from 'lucide-react';
function cn(...classes: (string | false | null | undefined)[]) {
return classes.filter(Boolean).join(' ');
}
type Metric = {
id: string;
label: string;
value: string;
trend: number;
compare: string;
sparkline: number[];
};
const METRICS: Metric[] = [
{ id: 'revenue', label: 'Revenue this month', value: '$12,453.20', trend: 12.4, compare: 'vs last month', sparkline: [40, 55, 35, 60, 45, 70, 50, 75, 65, 80, 70, 95] },
{ id: 'subscribers', label: 'Active subscribers', value: '2,847', trend: 8.2, compare: 'vs last month', sparkline: [60, 62, 65, 64, 68, 71, 70, 74, 76, 75, 79, 82] },
{ id: 'mrr', label: 'Monthly recurring revenue', value: '$48,392', trend: -2.1, compare: 'vs last month', sparkline: [80, 82, 85, 84, 82, 78, 75, 73, 75, 72, 70, 68] },
];
export function BigMetricCard() {
const [activeId, setActiveId] = useState<string>('revenue');
const metric = METRICS.find((m) => m.id === activeId) ?? METRICS[0];
const isUp = metric.trend >= 0;
return (
<div className="w-full max-w-[420px] bg-zinc-900 border border-white/[0.08] rounded-2xl overflow-hidden">
<div className="flex items-center gap-1 px-3 pt-3 border-b border-white/[0.06]">
{METRICS.map((m) => (
<button
key={m.id}
onClick={() => setActiveId(m.id)}
className={cn(
'relative px-2.5 pt-1 pb-3 text-xs font-medium transition-colors',
activeId === m.id ? 'text-white' : 'text-zinc-500 hover:text-zinc-300'
)}
>
{m.label.split(' ')[0]}
{activeId === m.id && (
<span className="absolute bottom-0 left-1 right-1 h-[2px] rounded-full bg-indigo-500" />
)}
</button>
))}
</div>
<div className="px-6 pt-6 pb-2">
<p className="text-[11px] font-medium tracking-wide text-zinc-500 uppercase">{metric.label}</p>
<p
className="mt-2 text-4xl font-bold text-white tabular-nums tracking-[-0.02em]"
style={{ fontVariantNumeric: 'tabular-nums' }}
>
{metric.value}
</p>
<div className="mt-3 flex items-center gap-2">
<span
className={cn(
'inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] font-semibold',
isUp
? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'
: 'bg-red-500/10 text-red-400 border border-red-500/20'
)}
>
{isUp ? <TrendingUp className="w-3 h-3" /> : <TrendingDown className="w-3 h-3" />}
{isUp ? '+' : ''}
{metric.trend.toFixed(1)}%
</span>
<span className="text-[11px] text-zinc-500">{metric.compare}</span>
</div>
</div>
<div className="px-2 pt-2 pb-3">
<Sparkline points={metric.sparkline} isUp={isUp} />
</div>
</div>
);
}
function Sparkline({ points, isUp }: { points: number[]; isUp: boolean }) {
const W = 380;
const H = 56;
const PADDING = 4;
const max = Math.max(...points);
const min = Math.min(...points);
const range = max - min || 1;
const xStep = (W - PADDING * 2) / (points.length - 1);
const coords = points.map((v, i) => {
const x = PADDING + i * xStep;
const y = PADDING + (1 - (v - min) / range) * (H - PADDING * 2);
return { x, y };
});
const linePath = coords
.map((c, i) => `${i === 0 ? 'M' : 'L'} ${c.x.toFixed(2)} ${c.y.toFixed(2)}`)
.join(' ');
const areaPath =
`${linePath} L ${coords[coords.length - 1].x.toFixed(2)} ${H - PADDING} ` +
`L ${coords[0].x.toFixed(2)} ${H - PADDING} Z`;
const stroke = isUp ? '#10b981' : '#ef4444';
const fill = isUp ? 'rgba(16,185,129,0.12)' : 'rgba(239,68,68,0.12)';
const last = coords[coords.length - 1];
return (
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-14" aria-hidden>
<path d={areaPath} fill={fill} />
<path d={linePath} stroke={stroke} strokeWidth={1.5} fill="none" />
<circle cx={last.x} cy={last.y} r={2.5} fill={stroke} />
</svg>
);
}Unlock to copy
Free access to all patterns
Command Palette
Linear
Global Search with Results Grouping
Linear
Recent + Suggested Search
Raycast
Filter Builder (AND / OR)
Linear
Sidebar Navigation
Linear
Collapsible Nested Tree Nav
Notion
Tab Bar with Overflow Menu
Vercel
Breadcrumb with Dropdowns
Linear
Workspace Switcher
Slack
Deployment Status Card
Vercel
Empty State
Vercel
Slash Command Menu
Notion
Stats & Metrics Row
Vercel
Single Big Metric Card
Stripe
Confirmation Dialog
Linear
Slide-Over Panel
Stripe
Issue / Task Card
Linear
Activity Feed
Linear + Slack
Properties Panel
Notion + Linear
Inline Data Table
Notion + Linear