Skip to content
microcharts
ReferenceCoreDualSparkline

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.

DualSparkline
interactive · 5.88 kB · static · 3.5 kB

Install

Import & usereact/dual-sparkline
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];
Needs package + stylesheetSet up with AI

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

endpoint label vs normal-range band

Edge cases

benchmark ends early
gaps in both series
locale

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

PropTypeNotes
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").
seriesStringsSeriesStringsi18n strings for the per-series trend clauses.
animatebooleaninteractiveOpt-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