Sparkline
A trend at a glance, beside the number it belongs to: in a stat card, a table row or a line of text.
Open issues, ai-niche-scout
19
down from 31 in 12 weeks
CI minutes in October
2,840
up from 1,700 in March
- cennge-ui48
- spredo-crypto-screener15
- ai-niche-scout12
- payload-cache9
- playwright-msi-scraper3
- The trend is drawn in grey and the latest value is a dot in the accent: switch the accent at the top of the page.
- Given a color, the line and the dot both take it, as in the open issues card.
- No axes, no readout: each is one picture for a screen reader, named by its aria-label.
Field marks
- Import
import { Sparkline } from "cennge-ui/charts";- Size
- 1.9 KB gzip for this import, its dependencies aside
- Pattern
role="img"on an inline<span>, named byaria-label- SSR
- Rendered on the server in the Next.js check; hydrates without a warning
- Default box
h-8 w-24, inline with text; size it withclassName- Tests
- Unit tests; end-to-end in Chromium, Firefox and WebKit, on desktop and phone viewports: sparkline
- Source
- src/charts
When to use#
Use a Sparkline where a number needs its history beside it and there is no room for a chart: a stat card, a row per repository, a sentence. It draws the shape of the values from the lowest to the highest across its box, without axes, so it says which way something went, not by how much. When the reader needs the values, use a LineChart or put the number next to it.
Similar#
- LineChart has axes, a readout and keyboard access to every value.
- AreaChart with its axes and grid turned off fits in a card too, and keeps the readout.
- Progress is one value against a maximum, not a trend.
In a stat card#
variant="area" washes the area under the line in its colour. Give the box its size with className; here it takes the card's width. The numbers on this page are sample data.
CI runs, last 12 weeks
3,257
import { Card, CardBody } from "cennge-ui";
import { Sparkline } from "cennge-ui/charts";
const runs = [212, 198, 240, 251, 236, 270, 288, 276, 301, 322, 315, 348];
const total = runs.reduce((sum, week) => sum + week, 0);
export default function CiRunsCard() {
return (
<Card variant="flat" className="w-full max-w-64">
<CardBody>
<p className="text-sm text-muted">CI runs, last 12 weeks</p>
<p className="text-2xl font-semibold tabular-nums">{total.toLocaleString("en-US")}</p>
<Sparkline
aria-label="CI runs per week over 12 weeks, up from 212 to 348"
data={runs}
variant="area"
curve="smooth"
className="mt-2 h-12 w-full"
/>
</CardBody>
</Card>
);
}In a line of text#
The Sparkline is an inline <span>, set on the middle of the line, so it sits in running text. Keep it about as tall as the text.
payload-cache answered 1.2 million requests last week and 1.1 million the week before.
import { Sparkline } from "cennge-ui/charts";
// Millions of requests a week, the oldest first.
const requests = [0.9, 0.95, 0.93, 1.02, 1.08, 1.05, 1.14, 1.2];
export default function InlineTrend() {
return (
<p className="max-w-md text-sm leading-relaxed">
payload-cache answered 1.2 million requests last week{" "}
<Sparkline
aria-label="Requests per week over 8 weeks, up by a third"
data={requests}
className="mx-0.5 h-4 w-14"
/>{" "}
and 1.1 million the week before.
</p>
);
}Colours that mean something#
By default the trend is grey and the latest value takes the accent. With color both take it: a theme colour that says whether the change is good or bad. The words beside it say the same, so colour is never the only sign.
- Failed runs a daydown from 92
- p95 response timeup from 212 ms301 ms
import { Sparkline } from "cennge-ui/charts";
const trends = [
{
name: "Failed runs a day",
now: "2",
change: "down from 9",
data: [9, 7, 8, 6, 5, 6, 4, 3, 4, 2],
color: "success",
},
{
name: "p95 response time",
now: "301 ms",
change: "up from 212 ms",
data: [212, 205, 219, 231, 228, 246, 263, 270, 287, 301],
color: "danger",
},
] as const;
export default function GoodAndBad() {
return (
<ul className="flex w-full max-w-md flex-col gap-3">
{trends.map((trend) => (
<li key={trend.name} className="flex items-center gap-4 text-sm">
<span className="min-w-0 flex-1">
<span className="block font-medium">{trend.name}</span>
<span className="text-muted">{trend.change}</span>
</span>
<Sparkline
aria-label={`${trend.name}, ${trend.change} to ${trend.now}`}
data={trend.data}
color={trend.color}
className="h-8 w-28"
/>
<span className="w-16 text-right font-semibold tabular-nums">{trend.now}</span>
</li>
))}
</ul>
);
}Gaps#
null where a value is missing breaks the line; a value alone between two gaps shows as a small dot. showLast={false} drops the dot on the latest value.
import { Sparkline } from "cennge-ui/charts";
// Monitoring was down on the 4th and 5th day, and again on the 7th: null, not 0.
const errors = [3, 5, 4, null, null, 6, null, 2, 3, 1];
export default function ErrorsWithGaps() {
return (
<div className="flex items-center gap-3 text-sm">
<span className="text-muted">Scraper errors a day</span>
<Sparkline
aria-label="Scraper errors a day over 10 days, with three days missing"
data={errors}
className="h-10 w-40"
/>
</div>
);
}Accessibility#
A Sparkline is role="img" and its SVG is hidden, so a screen reader hears the aria-label and nothing else. Write the label as the reader should hear the trend: what it shows, over how long, and which way it went, as "CI runs per week over 12 weeks, up from 212 to 348". When the values themselves matter, give them in text or a table beside it.
Server rendering#
The whole Sparkline is rendered on the server: the line is drawn in a box of its own that stretches to the element, so nothing waits for a measure. The dots are placed in percent, so they stay round however the box is stretched.