A tiny line or bar graphic that shows the shape of a trend, without axes. Decoration unless it has a label.
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 sparklineCopies 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.jsonWorks, 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
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
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
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
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
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.
| Prop | Type | Default |
|---|---|---|
datarequiredThe values, oldest first. Non-numbers are ignored. | number[] | none |
typeA line, a line with a soft area under it, or thin bars. | "line" | "area" | "bar" | "line" |
labelA text description, e.g. "Revenue, last 12 months: from 21K to 48K". Without it the graphic is hidden from assistive technology. | string | none |
widthWidth in pixels. | number | 96 |
responsiveFills the width of its parent and keeps the aspect ratio of width and height. Use it for a wide chart in a KPI row. | boolean | false |
heightHeight in pixels. | number | 32 |
colorColor of the line or bars. | "primary" | "success" | "danger" | "muted" | "primary" |
showLastMarks the last point of a line with a dot. | boolean | true |
showExtremesMark the highest and lowest points with small rings, so the range shows without reading numbers. | boolean | false |
showAverageDraw a dashed line at the average, to see which points sit above and below it. | boolean | false |
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