Skip to content

Sparkline

A chart small enough to read as a word: no axes, no legend, no title — just the shape of a series, next to the number it belongs to.

Requests are up over the last twelve weeks while latency has come down .

import { Sparkline } from '@ontoui/react';
<Sparkline.Root data={[12, 18, 15, 26, 24, 33, 30, 41]}>
<Sparkline.Line />
<Sparkline.Spot />
</Sparkline.Root>;

Anatomy

PartDescription
Sparkline.Root<svg role="img"> — the box, the scale, and the accessible name.
Sparkline.Line<path> — the series as a stroked line.
Sparkline.Area<path> — the fill between the line and the baseline.
Sparkline.Bars<g> of <rect> — the series as bars standing on the baseline.
Sparkline.Spot<circle> — a dot on one point: the last reading, the peak, the low.
Sparkline.Baseline<line> — a rule at zero, a target, or an average.

SVG paints in document order, so the parts are stacked in the order you write them: the area before the line it sits under, the baseline before the marks that cross it.

<Sparkline.Root data={data}>
<Sparkline.Baseline />
<Sparkline.Area />
<Sparkline.Line />
<Sparkline.Spot />
</Sparkline.Root>

A sparkline is a trend, not a reading. It says up, and steeply toward the end — never 47.2. Put the number next to it in text; the chart carries the shape.

Data

data is a plain array of numbers, oldest first and evenly spaced. There is no x axis and no notion of a date, so a gappy series has to arrive with its holes already in it: null — and any value that is not a finite number — keeps its place on the axis and is not drawn.

The weeks with no reading keep their place on the axis, and the line breaks rather than drawing across them.
<Sparkline.Root data={[12, 18, null, null, 24, 33, 30, null, 38, 47]}>
<Sparkline.Line />
<Sparkline.Spot />
</Sparkline.Root>

The line breaks at a gap rather than drawing across it, and a reading left alone between two gaps is drawn as a dot — it is still a reading. Sparkline.Spot skips gaps too, so a series that ends in one still marks the last value there was.

Marks

<Sparkline.Root data={data}>
<Sparkline.Line />
</Sparkline.Root>
<Sparkline.Root data={data}>
<Sparkline.Area />
<Sparkline.Line />
<Sparkline.Spot />
</Sparkline.Root>
<Sparkline.Root data={data}>
<Sparkline.Bars />
</Sparkline.Root>

Use a line for something continuous that is read as a shape — a rate, a price, a latency. Use bars for counts read one at a time — deploys per day, errors per hour. Sparkline.Bars takes its width from a gap that is given up before the bar is, so a hundred points still render as hairlines rather than vanishing.

Sparkline.Spot is positioned from the line’s points, so pair it with Sparkline.Line rather than with bars. It marks last by default, and takes first, min, max, or an index.

Scale

The range is taken from the data: the smallest value sits at the bottom of the box and the largest at the top, so the shape fills the space it has. That makes two sparklines comparable only when they are given the same bounds — pass min and max when a row of them has to be read against each other, or when a flat week should look flat rather than dramatic.

<Sparkline.Root data={data} min={0} max={100}>

baseline is what bars stand on and areas fill down to. It defaults to 0 when the range spans it, and to the bottom of the range otherwise — so a series that crosses zero grows in both directions without any configuration.

Zero falls inside the range, so the bars stand on it — the negative weeks hang below.
<Sparkline.Root data={[-4, -2, 1, 3, -1, 5, 6, 2, -3, 4, 7, 9]} width={160} height={40}>
<Sparkline.Bars />
<Sparkline.Baseline />
</Sparkline.Root>

Sparkline.Baseline takes a value of its own for a target or an average, and stays inside the box even when that value is outside the range.

Size

width and height are pixels, and they default to 80 × 20 — the size of a few words of text. The geometry is drawn from them, so they are props rather than stylesheet overrides.

<Sparkline.Root data={data} width={148} height={36}>

padding keeps room inside the box so a stroke or a dot at the very top of the range is not clipped by the edge. Raise it along with r if you enlarge the spot.

In a metric card, size the chart to the card and hide it from screen readers — the value and the trend are already written above it:

Requests / week52k
p95 latency118 ms
<div className="card">
<span className="card-label">Requests / week</span>
<span className="card-value">52k</span>
<Sparkline.Root data={requests} width={148} height={36} aria-hidden>
<Sparkline.Area />
<Sparkline.Line />
<Sparkline.Spot />
</Sparkline.Root>
</div>

In a data grid

A sparkline is at its most useful in a column: one shape per row, all read down the page at once. Give every cell the same width and height so the column scans as a single chart.

ServiceErrorsLast 8h
checkout0.4%
search0.1%
billing1.2%
const columns: ColumnDef<Service, unknown>[] = [
{ accessorKey: 'name', header: 'Service' },
{ accessorKey: 'errors', header: 'Errors' },
{
accessorKey: 'trend',
header: 'Last 8h',
cell: ({ row }) => (
<Sparkline.Root data={row.original.trend} width={72} height={18}>
<Sparkline.Line />
<Sparkline.Spot />
</Sparkline.Root>
),
},
];

Rows are scaled independently unless you pass min and max, so a column of sparklines compares shapes, not sizes. When the column is meant to compare magnitudes, bound every row to the same range.

Color

<Sparkline.Root data={data} color="danger">

Color is decorative — the accessible name reads the same at any tint. Keep the meaning in the text beside the chart: a red spark next to 1.2% errors, never a red spark on its own.

Styling

Every part is an ordinary SVG element, so its stroke, fill and width are ordinary CSS. Pass a className to the part you want to change:

<Sparkline.Root data={data}>
<Sparkline.Line className="thick" />
</Sparkline.Root>
.thick {
stroke-width: 2.5;
}

The root publishes its tint as --sparkline-color, which every part draws from, so one declaration restyles the whole series:

.brand {
--sparkline-color: #7c3aed;
}

Accessibility

  • The chart is one image. Sparkline.Root is role="img" with a name worded from the series — “Sparkline: 12 points, from 18 to 46, trending up (low 12, high 51)”. The shapes inside it are not announced separately.
  • Say what the series is. The generated name describes the numbers, not the subject. Pass getAriaLabel to name it: (s) => `Weekly requests, ${s.direction}, now ${s.format(s.last!)}`.
  • Hide it when the numbers are already written. In a metric card the value and the delta are text on the same card, so aria-hidden on the chart avoids reading them twice.
  • format and locale reach the announced numbers. A chart of currency or milliseconds should say so out loud, the same way the label beside it does.
  • Forced colors are handled. The marks fall back to system ink, so the chart does not go blank in a high-contrast theme.

API Reference

Sparkline.Root

Prop Type Default
children? ReactNode
className? string
style? CSSProperties
data readonly (number | null | undefined)[]
width? number 80
height? number 20
padding? number 3
min? number the smallest value in `data`
max? number the largest value in `data`
baseline? number `0` when the range spans it, and the bottom of the range otherwise
color? "default" | "success" | "warning" | "danger" 'default'
format? NumberFormatOptions
locale? LocalesArgument the runtime locale
getAriaLabel? (summary: SparklineSummary) => string
ref? Ref<SVGSVGElement>

Sparkline.Line

Prop Type Default
className? string
style? CSSProperties
ref? Ref<SVGSVGElement>

Sparkline.Area

Prop Type Default
className? string
style? CSSProperties
ref? Ref<SVGSVGElement>

Sparkline.Bars

Prop Type Default
className? string
style? CSSProperties
gap? number 2
radius? number 1
ref? Ref<SVGSVGElement>

Sparkline.Spot

Prop Type Default
className? string
style? CSSProperties
point? SparklineSpotPoint 'last'
r? number 2
ref? Ref<SVGSVGElement>

Sparkline.Baseline

Prop Type Default
className? string
style? CSSProperties
value? number the root's `baseline`
ref? Ref<SVGSVGElement>