Sparkline Trend Cards

A grid of compact metric cards, each with a label, bold value, an inline SVG sparkline, and a color-coded trend delta pill. The sparkline stroke and fill flip between green (up) and red (down) based on the delta sign. No chart library needed.

Dashboard StatsInspired by Linear
Live PreviewInteractive
Linear · Analytics

MRR

$48.2K

+3.1%vs prev. week

Active Users

2,847

+8.4%vs prev. week

Churn Rate

1.8%

-0.3%vs prev. week

ARR

$578K

+12.1%vs prev. week

Switch timeframes to update sparklines · Green/red pill reflects delta direction

SparklineTrendCards.tsx
// Dependencies: react ^18, lucide-react
import React from 'react';
import { TrendingUp, TrendingDown } from 'lucide-react';

function cn(...classes: (string | false | null | undefined)[]) {
  return classes.filter(Boolean).join(' ');
}

export type TrendCard = {
  id: string;
  label: string;
  value: string;
  delta: number;
  deltaLabel?: string;
  sparkline: number[];
};

type Props = {
  cards: TrendCard[];
  columns?: 2 | 3 | 4;
};

const COL_CLASS: Record<number, string> = {
  2: 'grid-cols-2',
  3: 'grid-cols-3',
  4: 'grid-cols-4',
};

function Sparkline({ points, isUp }: { points: number[]; isUp: boolean }) {
  const W = 100;
  const H = 32;
  const max = Math.max(...points);
  const min = Math.min(...points);
  const range = max - min || 1;

  const coords = points.map((v, i) => ({
    x: (i / (points.length - 1)) * W,
    y: (1 - (v - min) / range) * H,
  }));

  const linePath = coords
    .map((c, i) => `${i === 0 ? 'M' : 'L'} ${c.x.toFixed(1)} ${c.y.toFixed(1)}`)
    .join(' ');

  const areaPath =
    `${linePath} L ${coords[coords.length - 1].x.toFixed(1)} ${H} L 0 ${H} Z`;

  const stroke = isUp ? '#10b981' : '#ef4444';
  const fill = isUp ? 'rgba(16,185,129,0.12)' : 'rgba(239,68,68,0.12)';

  return (
    <svg viewBox={`0 0 ${W} ${H}`} className="w-full h-8" aria-hidden>
      <path d={areaPath} fill={fill} />
      <path d={linePath} stroke={stroke} strokeWidth={1.5} fill="none" />
    </svg>
  );
}

export function SparklineTrendCards({ cards, columns = 2 }: Props) {
  return (
    <div className={cn('grid gap-3', COL_CLASS[columns] ?? 'grid-cols-2')}>
      {cards.map((card) => {
        const isUp = card.delta >= 0;
        return (
          <div
            key={card.id}
            className="flex flex-col gap-2 rounded-xl border border-[#1a1a1a] bg-[#0a0a0a] p-4"
          >
            <p className="text-[11px] font-medium text-zinc-500 leading-none">{card.label}</p>
            <p className="text-2xl font-bold text-white tabular-nums tracking-tight leading-none">{card.value}</p>
            <Sparkline points={card.sparkline} isUp={isUp} />
            <div className="flex items-center gap-1.5">
              <span
                className={cn(
                  'inline-flex items-center gap-0.5 rounded-md px-1.5 py-0.5 text-[11px] font-semibold border',
                  isUp
                    ? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20'
                    : 'bg-red-500/10 text-red-400 border-red-500/20',
                )}
              >
                {isUp ? <TrendingUp className="w-3 h-3" /> : <TrendingDown className="w-3 h-3" />}
                {isUp ? '+' : ''}{card.delta.toFixed(1)}%
              </span>
              {card.deltaLabel && (
                <span className="text-[11px] text-zinc-600">{card.deltaLabel}</span>
              )}
            </div>
          </div>
        );
      })}
    </div>
  );
}

// Usage:
// const CARDS: TrendCard[] = [
//   { id: 'mrr', label: 'MRR', value: '$48.2K', delta: 3.1, deltaLabel: 'vs last month', sparkline: [40, 38, 45, 42, 50, 53, 55] },
//   { id: 'users', label: 'Active Users', value: '2,847', delta: 8.4, sparkline: [200, 210, 230, 240, 260, 270, 285] },
// ];
// <SparklineTrendCards cards={CARDS} columns={2} />