Single Big Metric Card

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.

Dashboard StatsInspired by Stripe
Live PreviewInteractive
Stripe · Dashboard

Revenue this month

$12,453.20

+12.4%vs last month

Tap a metric to switch · Trend pill turns red on negative · Sparkline color follows direction

BigMetricCard.tsx
// 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