Skip to content

Sparkline

A tiny line or bar graphic that shows the shape of a trend, without axes. Decoration unless it has a label.

Datav0.1.0experimentalWCAG 2.2 AAView spec
Daily revenue, last 60 days$62.1K+42%
import { Sparkline } from "@rdloom/react";

// Fixed, repeatable series so the preview never changes between renders.
const noise = (i: number, k: number) => Math.sin(i * 12.9898 + k * 78.233) * 0.5 + Math.sin(i * 4.1 + k) * 0.5;

// Daily revenue in thousands: lower at weekends, on top of slow growth, 60 days.
const revenue = Array.from({ length: 60 }, (_, i) => {
  const weekend = i % 7 === 5 || i % 7 === 6 ? -6 : 0;
  return Math.round((42 + i * 0.35 + weekend + noise(i, 1) * 3) * 10) / 10;
});

// Give it a label when the picture carries information the page doesn't say in words.
export default function SparklineLineExample() {
  const last = revenue[revenue.length - 1];
  const change = Math.round(((last - revenue[0]) / revenue[0]) * 100);
  return (
    <div className="flex w-full justify-center">
      <div className="flex w-[36rem] max-w-full flex-col gap-3">
        <div className="flex items-baseline justify-between gap-4">
          <span className="text-sm text-[var(--rd-color-text-muted)]">Daily revenue, last 60 days</span>
          <span className="flex items-baseline gap-2 tabular-nums">
            <span className="text-2xl font-semibold">${last}K</span>
            <span className="text-sm text-[var(--rd-color-feedback-success)]">+{change}%</span>
          </span>
        </div>
        <Sparkline responsive width={576} height={96} data={revenue} label={`Daily revenue, last 60 days: from ${revenue[0]}K to ${last}K, lower on weekends`} />
      </div>
    </div>
  );
}

Installation

npx rdloom add sparkline

Copies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes.

Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/sparkline.json

Works, but without upgrade tracking.

Usage

import { Sparkline } from "@rdloom/react";

<div className="flex w-full justify-center">
  <div className="flex w-[36rem] max-w-full flex-col gap-3">
    <div className="flex items-baseline justify-between gap-4">
      <span className="text-sm text-[var(--rd-color-text-muted)]">Daily revenue, last 60 days</span>
      <span className="flex items-baseline gap-2 tabular-nums">
        <span className="text-2xl font-semibold">${last}K</span>
        <span className="text-sm text-[var(--rd-color-feedback-success)]">+{change}%</span>
      </span>
    </div>
    <Sparkline responsive width={576} height={96} data={revenue} label={`Daily revenue, last 60 days: from ${revenue[0]}K to ${last}K, lower on weekends`} />
  </div>
</div>

Area

Signups, last 90 days19,750+2% vs previous week
import { Sparkline } from "@rdloom/react";

// Fixed, repeatable series so the preview never changes between renders.
const noise = (i: number, k: number) => Math.sin(i * 12.9898 + k * 78.233) * 0.5 + Math.sin(i * 4.1 + k) * 0.5;

// New signups per day over 90 days, trending up with a launch bump around day 45.
const signups = Array.from({ length: 90 }, (_, i) =>
  Math.max(0, Math.round(120 + i * 2.1 + (i > 45 && i < 55 ? 60 : 0) + noise(i, 2) * 18)),
);

export default function SparklineAreaExample() {
  const total = signups.reduce((n, v) => n + v, 0);
  const lastWeek = signups.slice(-7).reduce((n, v) => n + v, 0);
  const prevWeek = signups.slice(-14, -7).reduce((n, v) => n + v, 0);
  const change = Math.round(((lastWeek - prevWeek) / prevWeek) * 100);
  return (
    <div className="flex w-full justify-center">
      <div className="flex w-[36rem] max-w-full flex-col gap-3">
        <div className="flex items-baseline justify-between gap-4">
          <span className="text-sm text-[var(--rd-color-text-muted)]">Signups, last 90 days</span>
          <span className="flex items-baseline gap-2 tabular-nums">
            <span className="text-2xl font-semibold">{total.toLocaleString("en-US")}</span>
            <span className="text-sm text-[var(--rd-color-feedback-success)]">+{change}% vs previous week</span>
          </span>
        </div>
        <Sparkline type="area" responsive width={576} height={96} data={signups} label={`Signups per day, last 90 days: from ${signups[0]} to ${signups[89]}, with a bump after day 45`} />
      </div>
    </div>
  );
}

Bar

Orders per day, last 45 days63orders a day on average
import { Sparkline } from "@rdloom/react";

// Fixed, repeatable series so the preview never changes between renders.
const noise = (i: number, k: number) => Math.sin(i * 12.9898 + k * 78.233) * 0.5 + Math.sin(i * 4.1 + k) * 0.5;

// Orders per day for 45 days, busier on weekdays.
const orders = Array.from({ length: 45 }, (_, i) => {
  const weekend = i % 7 === 5 || i % 7 === 6;
  return Math.round((weekend ? 38 : 72) + noise(i, 3) * 10);
});

export default function SparklineBarExample() {
  const avg = Math.round(orders.reduce((n, v) => n + v, 0) / orders.length);
  return (
    <div className="flex w-full justify-center">
      <div className="flex w-[36rem] max-w-full flex-col gap-3">
        <div className="flex items-baseline justify-between gap-4">
          <span className="text-sm text-[var(--rd-color-text-muted)]">Orders per day, last 45 days</span>
          <span className="flex items-baseline gap-2 tabular-nums">
            <span className="text-2xl font-semibold">{avg}</span>
            <span className="text-sm text-[var(--rd-color-text-muted)]">orders a day on average</span>
          </span>
        </div>
        <Sparkline type="bar" responsive width={576} height={80} data={orders} label={`Orders per day, last 45 days: about ${avg} on average, fewer at weekends`} />
      </div>
    </div>
  );
}

Colors

  • RevenueUp$58.2K+24%
  • Active usersUp12,480+11%
  • ChurnRising2.4%+0.9 pt
  • Page viewsFlat84.1K0%
import { Sparkline } from "@rdloom/react";

// Fixed, repeatable series so the preview never changes between renders.
const noise = (i: number, k: number) => Math.sin(i * 12.9898 + k * 78.233) * 0.5 + Math.sin(i * 4.1 + k) * 0.5;

const series = (n: number, base: number, slope: number, amp: number, k: number) =>
  Array.from({ length: n }, (_, i) => Math.round((base + slope * i + noise(i, k) * amp) * 100) / 100);

// Color says "good", "bad" or "just a trend", but the words around it still say so: never rely on color alone.
const rows = [
  { label: "Revenue", value: "$58.2K", change: "+24%", note: "Up", color: "primary" as const, data: series(60, 42, 0.28, 3, 1) },
  { label: "Active users", value: "12,480", change: "+11%", note: "Up", color: "success" as const, data: series(60, 9000, 58, 220, 2) },
  { label: "Churn", value: "2.4%", change: "+0.9 pt", note: "Rising", color: "danger" as const, data: series(60, 1.4, 0.017, 0.12, 3) },
  { label: "Page views", value: "84.1K", change: "0%", note: "Flat", color: "muted" as const, data: series(60, 84, 0, 4, 4) },
];

export default function SparklineColorsExample() {
  return (
    <div className="flex w-full justify-center">
      <ul className="flex w-[36rem] max-w-full flex-col divide-y divide-[var(--rd-color-border-default)] text-sm">
        {rows.map((r) => (
          <li key={r.label} className="grid grid-cols-[7rem_1fr_6rem] items-center gap-4 py-3">
            <span className="flex flex-col">
              <span className="font-medium">{r.label}</span>
              <span className="text-xs text-[var(--rd-color-text-muted)]">{r.note}</span>
            </span>
            <Sparkline type="area" color={r.color} responsive width={240} height={40} data={r.data} />
            <span className="flex flex-col items-end tabular-nums">
              <span className="font-semibold">{r.value}</span>
              <span className="text-xs text-[var(--rd-color-text-muted)]">{r.change}</span>
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}

Sizes

96 × 28
240 × 48
576 × 80
import { Sparkline } from "@rdloom/react";

// Fixed, repeatable series so the preview never changes between renders.
const noise = (i: number, k: number) => Math.sin(i * 12.9898 + k * 78.233) * 0.5 + Math.sin(i * 4.1 + k) * 0.5;

const data = Array.from({ length: 40 }, (_, i) => Math.round((60 + i * 0.9 + noise(i, 5) * 6) * 10) / 10);

// You choose the size in pixels, or pass responsive to fill the parent. Small ones sit in text, large ones stand on their own.
export default function SparklineSizesExample() {
  return (
    <div className="flex w-full justify-center">
      <div className="flex w-[36rem] max-w-full flex-col gap-5">
        {[
          { w: 96, h: 28 },
          { w: 240, h: 48 },
          { w: 576, h: 80 },
        ].map(({ w, h }) => (
          <figure key={w} className="flex items-center gap-4">
            <figcaption className="w-24 shrink-0 text-xs tabular-nums text-[var(--rd-color-text-muted)]">
              {w} × {h}
            </figcaption>
            <Sparkline type="area" width={w} height={h} data={data} className="max-w-full" />
          </figure>
        ))}
      </div>
    </div>
  );
}

With extremes

p95 latency, last 24 hours126 msavg, peak 391 ms, low 104 ms
import { Sparkline } from "@rdloom/react";

// Fixed, repeatable series so the preview never changes between renders.
const noise = (i: number, k: number) => Math.sin(i * 12.9898 + k * 78.233) * 0.5 + Math.sin(i * 4.1 + k) * 0.5;

// API latency in ms, one point every 20 minutes for a day, with a few spikes.
const latency = Array.from({ length: 72 }, (_, i) => {
  const spike = i === 21 ? 260 : i === 22 ? 140 : i === 51 ? 190 : 0;
  return Math.round(118 + noise(i, 4) * 14 + spike);
});

// The rings mark the slowest and fastest points, and the dashed line shows where "normal" is.
export default function SparklineWithExtremesExample() {
  const hi = Math.max(...latency);
  const lo = Math.min(...latency);
  const avg = Math.round(latency.reduce((n, v) => n + v, 0) / latency.length);
  return (
    <div className="flex w-full justify-center">
      <div className="flex w-[36rem] max-w-full flex-col gap-3">
        <div className="flex items-baseline justify-between gap-4">
          <span className="text-sm text-[var(--rd-color-text-muted)]">p95 latency, last 24 hours</span>
          <span className="flex items-baseline gap-2 tabular-nums">
            <span className="text-2xl font-semibold">{avg} ms</span>
            <span className="text-sm text-[var(--rd-color-text-muted)]">avg, peak {hi} ms, low {lo} ms</span>
          </span>
        </div>
        <Sparkline responsive width={576} height={96} data={latency} showExtremes showAverage color="danger" label={`p95 latency over 24 hours: average ${avg} ms, peak ${hi} ms, low ${lo} ms`} />
      </div>
    </div>
  );
}

In a sentence

Traffic is up 18% this week

import { Sparkline } from "@rdloom/react";

// Next to text that already says it, the graphic is decoration: no label, so a screen reader skips it.
export default function SparklineInASentenceExample() {
  return (
    <p className="flex items-center gap-2 text-sm">
      Traffic is up 18% this week
      <Sparkline data={[40, 42, 39, 45, 50, 52, 58]} width={64} height={22} color="success" />
    </p>
  );
}

In a table

CustomerPlanLast 7 daysChange
Ada LovelaceTeamUp 125%
Grace HopperProDown 40%
Alan TuringProUp 11%
import { Sparkline, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from "@rdloom/react";

const rows = [
  { id: "a", name: "Ada Lovelace", plan: "Team", usage: [12, 15, 14, 19, 22, 21, 27] },
  { id: "b", name: "Grace Hopper", plan: "Pro", usage: [30, 28, 26, 27, 22, 20, 18] },
  { id: "c", name: "Alan Turing", plan: "Pro", usage: [9, 9, 10, 9, 10, 9, 10] },
];

// A column of small trends. Each row's trend is described in the text beside it, so the graphics stay decoration.
export default function SparklineInATableExample() {
  return (
    <div className="w-[34rem] max-w-full">
      <Table label="Usage by customer">
        <TableHeader>
          <TableColumn isRowHeader>Customer</TableColumn>
          <TableColumn>Plan</TableColumn>
          <TableColumn>Last 7 days</TableColumn>
          <TableColumn>Change</TableColumn>
        </TableHeader>
        <TableBody>
          {rows.map((r) => {
            const change = Math.round(((r.usage[6] - r.usage[0]) / r.usage[0]) * 100);
            return (
              <TableRow key={r.id} id={r.id}>
                <TableCell>{r.name}</TableCell>
                <TableCell>{r.plan}</TableCell>
                <TableCell>
                  <Sparkline type="area" data={r.usage} width={64} height={28} color={change > 5 ? "success" : change < -5 ? "danger" : "muted"} />
                </TableCell>
                <TableCell className="tabular-nums">{change > 0 ? `Up ${change}%` : change < 0 ? `Down ${Math.abs(change)}%` : "No change"}</TableCell>
              </TableRow>
            );
          })}
        </TableBody>
      </Table>
    </div>
  );
}

API Reference

Defined by the spec. Components also accept the props of the React Aria component they wrap.

PropTypeDefault
datarequired

The values, oldest first. Non-numbers are ignored.

number[]none
type

A line, a line with a soft area under it, or thin bars.

"line" | "area" | "bar""line"
label

A text description, e.g. "Revenue, last 12 months: from 21K to 48K". Without it the graphic is hidden from assistive technology.

stringnone
width

Width in pixels.

number96
responsive

Fills the width of its parent and keeps the aspect ratio of width and height. Use it for a wide chart in a KPI row.

booleanfalse
height

Height in pixels.

number32
color

Color of the line or bars.

"primary" | "success" | "danger" | "muted""primary"
showLast

Marks the last point of a line with a dot.

booleantrue
showExtremes

Mark the highest and lowest points with small rings, so the range shows without reading numbers.

booleanfalse
showAverage

Draw a dashed line at the average, to see which points sit above and below it.

booleanfalse

Accessibility

Role img, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.

Keyboard

Not interactive.

Screen readers announce

  • With a label: announced as an image, e.g. "Revenue, last 12 months: from 21K to 48K, image"
  • Without a label: not announced

What your code must do

  • With a label it is an image with that name; the label should state the range and direction, not just the topic
  • Without a label it is hidden from assistive technology: the surrounding text must already say what the number did
  • The shape is never the only way to get the information; put the figures in text nearby
  • Not interactive: no focus, no tooltip

Guidelines

Use it when

  • Showing the recent shape of a number inside a Stat, table row or sentence
  • Where space is too tight for a full chart

Avoid it when

  • People need to read exact values: use a Chart or a Table
  • The trend is the main content of the page

Don't

  • Leaving out the label when no nearby text describes the trend
  • Using it as the only source of the figures
  • Comparing sparklines drawn on different scales as if they were equal

Design tokens

The semantic tokens this component uses. Change them once and every component follows; see Design tokens.

  • --rd-color-chart-1
  • --rd-color-feedback-success
  • --rd-color-feedback-danger
  • --rd-color-text-muted