DualSparkline
React dual sparkline chart — two related trends sharing one word-sized frame. Tiny accessible SVG microchart; RSC-safe static or /interactive.
DualSparkline answers "how is this series doing against its benchmark?" Exactly two series, ever — three overlapped lines at 16 px are unreadable (SparkGroup for that). The benchmark whispers: dashed, thinner, neutral — never distinguished by color alone.
Install
import { DualSparkline } from "@microcharts/react/dual-sparkline";// ours, market — real values under “Sample data” below<DualSparkline data={ours} compare={market} title="Conversion vs market" />Sample data
const ours = [12, 13, 12.4, 14, 15.2, 14.8, 16, 17.5, 17, 18.4, 19, 21];const market = [12, 12.4, 12.8, 13.1, 13.6, 14, 14.2, 14.8, 15, 15.4, 15.8, 16];Try it
When to use it
- Good for — metric-vs-benchmark in table cells, actual-vs-plan in KPI cards.
- Avoid for — 3+ series (SparkGroup), or different units per series (never dual axes).
Sizing
Variants
Edge cases
Each series carries its own gaps independently — a null in data breaks only the primary line at that index, a null
in compare only the benchmark, and the nearest-x hover still reports both values (or "no data" for whichever side is
missing). With a locale, the endpoint label and both hover values follow that locale's grouping.
Four homes
Why this default
One shared domain across both series — the entire point is comparability, so there are no dual axes and no per-series normalization. A shorter benchmark simply ends; stretching it would fake correlation. Coincident endpoints dedupe to one dot so ink never doubles.
Accessibility
The accessible name reads both trends and both endpoints — "Trending up 75% vs benchmark up 33%. Last 21 vs 16." Identical series read "Matching benchmark." The interactive entry announces pairs ("Point 9 of 12: 17 vs 15.").
The interactive entry follows the shared interaction contract:
arrow keys rove between units on both axes, Home and End jump to the ends, and a click, tap, Enter or
Space selects a unit — pinning its readout so it survives blur, until you select it again or press Escape.
On touch, a tap pins and a drag scrubs.
Props
| Prop | Type | Notes |
|---|---|---|
| data* | (number | null)[] | The series being judged. |
| compare* | (number | null)[] | The benchmark — dashed, thinner, neutral. |
| curve | "linear" | "smooth" | "step" | Line shape (default 'linear'). |
| band | [number, number] | Normal-range band behind both (shared grammar). |
| label | "last" | "none" | Endpoint value label for the primary series. |
| dots | "auto" | "none" | Endpoint dots on both lines (default "auto"). |
| seriesStrings | SeriesStrings | i18n strings for the per-series trend clauses. |
| animate | boolean | interactiveOpt-in entrance motion when the chart mounts client-side — add import "@microcharts/react/motion" once. Inert on the server, on hydrated server HTML, and under prefers-reduced-motion. |
Plus the shared grammar — data, domain, color, title, summary, format — and the layout props (width, height, className, style) that every chart accepts. Interactive entries also share animate and live, and — wherever a chart has more than one navigable unit — onActive, onSelect, selectedIndex and defaultSelectedIndex; and — wherever the chart shows a hover value — readout. See the shared grammar.
Related charts
BumpStrip
React bump strip chart — rank trajectory over time — #1 on top, gaps for unranked periods. Tiny accessible SVG microchart; RSC-safe static or /interactive.
StackedArea
React stacked area chart — composition shift over time, capped at three series. Tiny accessible SVG microchart; RSC-safe static or /interactive.