DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
ChartArea ChartBar ChartLine ChartLive Line ChartComposed ChartPie ChartFunnel ChartRadar ChartRadial ChartSankey ChartScatter ChartCandlestick ChartSunburst ChartChoropleth ChartActivity Heatmap
AI agentsContributing
  1. Components
  2. Line Chart

Line Chart

Token-driven line chart built on Recharts v3 — multi-series, step and curve interpolation, honest data gaps, y-axis domain control, threshold annotations, profit and loss colouring and full light/dark theming.

$ pnpm add @tessinaui/ui

When to use

  • A trend over a continuous axis. Time is the usual one. A line asks the reader to follow change, so use it when the shape of the movement is the point.
  • A handful of series compared on one scale. Revenue against expenses, this period against the last (draw the earlier one with dashedSeries).
  • Data with honest gaps. A null breaks the line instead of inventing a reading (Missing data).
  • A compact trend inside a card or table row. Switch the chrome off for the sparkline form.
  • Values that sit far from zero. A line encodes change, so autoMinValue or minValue is legitimate (Choosing a y-axis).
  • A result that changes sign. Profit and loss or cash flow, where the side of zero is the message (Profit and loss).

When not to use

  • For categories. Counts per region, team or product are BarChart; a line implies the points between them are meaningful.
  • To emphasise volume. A filled area under the line is AreaChart, not a prop here.
  • For a share of a whole. Use PieChart.
  • For a line read against bars. A total with its average or target, or a rate against the volume it comes from, is a ComposedChart.
  • For one value against a target. Use Meter or Progress.
  • For many series. Past the six series colours, group the rest into "other", split into small charts or toggle series on demand; more colours only make the lines harder to tell apart.
  • When the exact values matter more than the shape. Put the numbers in a Table and keep the chart as the summary.

Installation

Terminal
pnpm add @tessinaui/ui recharts

Usage

example.tsx
import { LineChart } from "@tessinaui/ui";
import type { ChartConfig } from "@tessinaui/ui";

const config: ChartConfig = {
  revenue: { label: "Revenue", color: "var(--chart-1)" },
  expenses: { label: "Expenses", color: "var(--chart-3)" },
};

const data = [
  { month: "Jan", revenue: 4200, expenses: 2400 },
  { month: "Feb", revenue: 3800, expenses: 2210 },
  { month: "Mar", revenue: 5100, expenses: 2290 },
];

<LineChart
  data={data}
  config={config}
  categoryKey="month"
  dashedSeries={["expenses"]}
  label="Revenue outgrew expenses every month this quarter"
/>

Examples

Default

A smooth, single-series line.

Multiple

Two series in brand tones; expenses dashed so it stays distinct without colour.

Curve

All six interpolations, including the three step alignments.

Sizes

Three aspect ratios — sm (short/wide), md, and lg (tall).

Dots and labels

Marker modes, and the three ways to print values on the plot.

Missing data

What a null does, and what connectNulls does to it.

Y-axis domain

Anchored at zero, fitted to the data, or pinned to a fixed window.

Axes

Titles, tick formatters, a right-hand axis, and end-only ticks.

Thresholds and bands

ReferenceLine and ReferenceArea passed as children.

Forecast

Actual and projected as two series, the projection dashed.

Period comparison

The previous period as a muted, dashed ghost.

Profit and loss

signColored colours one series by its sign: success above zero, error below, split exactly where the line crosses zero. The zero line is drawn in the axis ink and always stays in view. The tooltip's value leads with + or −, and its indicator takes the reading's colour. Pointing at Profit or Loss in the legend dims the other half.

Colour is never the only cue: a reading's side of the zero line and the sign in the tooltip both carry it. On a coloured plate whose ink replaces both hues, those are the only cues, so add dashedLoss there to dash the part below zero.

example.tsx
<LineChart
  data={months}
  config={{ net: { label: "Net result" } }}
  categoryKey="month"
  signColored
  dashedLoss
  valueFormatter={(value) => currency.format(value)}
/>

Series toggling

Toggles outside the legend, as real aria-pressed buttons.

Sparkline

The same component with its chrome switched off.

In a card

Range chips and the value callout belong to the card, not the chart.

Choosing a day

With selectionMode="single" a click or a tap anywhere over a day, or Enter or Space on the day the arrow keys are on, chooses it; choosing it again clears it. The chosen period keeps the hover band, each line's dot on it wears a ring, and its tick becomes the inverse chip, so the choice reads without colour. Here the list below shows the chosen day's oldest open tickets.

On a coloured plate

Inside a coloured Surface the chart's chrome takes the plate's ink with no prop: tick text, axis titles, grid lines, the hover cursor, the legend, printed values and the focus ring. Series colours are yours. The --chart-N tokens are measured against the page, not a plate, so give each series the plate's ink (hsl(var(--surface-ink))) and tell series apart with dashedSeries and a matching legend icon. The tooltip is a page panel, so it keeps the page's look.

States

Loading skeleton, empty state and error.

API Reference

PropTypeDefaultDescription
dataRecord<string, string | number | null>[]—Row-per-category data.
configChartConfig—Series metadata (label, color).
categoryKeystring—Datum key for the x-axis.
seriesstring[]all config keysWhich series to plot.
curve"monotone" | "natural" | "linear" | "step" | "stepBefore" | "stepAfter""monotone"Interpolation.
size"sm" | "md" | "lg""md"Aspect ratio.
dots"none" | "all" | "last""all"Point markers. "last" marks each series' final reading.
showLabelsboolean | "endpoints" | "extremes"falsePrint values on the plot.
dashedSeriesstring[][]Series rendered with a dashed stroke.
signColoredbooleanfalseColour the line by its sign, split at zero. Draws the first series only.
dashedLossbooleanfalseWith signColored, dash the part below zero.
valueFormatter(value, seriesKey) => stringtoLocaleString()Format tooltip values. With signColored it receives the magnitude and the chart writes the sign.
stringsPartial<LineChartStrings>English, Arabic under dir="rtl"The legend's Profit and Loss names, and selection's words: selected, cleared, items, period.
connectNullsbooleanfalseBridge null gaps with an interpolated segment.
autoMinValuebooleanfalseFit the y-axis to the data instead of anchoring at zero.
minValue / maxValuenumber—Pin either end of the y-axis.
yAxisPosition"left" | "right"follows dirWhich side the y-axis sits on.
xAxisLabel / yAxisLabelstring—Axis titles.
xTickFormatter / yTickFormatter(value, index) => string—Format tick text.
startEndOnlybooleanfalseRender only the first and last x ticks.
showGrid / showLegend / showXAxis / showYAxis / showTooltipboolean—Toggle chart chrome.
selectionMode"none" | "single""none""single" lets a reader choose one period by click, tap, Enter or Space; choosing it again clears it.
selectedstring | null—The categoryKey value of the chosen period, controlled. null chooses nothing.
defaultSelectedstring | nullnullThe categoryKey value of the period chosen at first render, uncontrolled.
onSelectedChange(key, { datum, index, source }) => void—Called on every choice and clear; source is "pointer" or "keyboard".
activestring | null—The categoryKey value of the lit period, controlled: opens the tooltip on it.
onActiveChange(key: string | null) => void—Called when the chart's own pointer or arrow keys move to another period.
childrenReactNode—Extra Recharts children (ReferenceLine, Brush, …).
dir"ltr" | "rtl"inheritedReading direction. Unset, read off the chart's position in the page once it mounts.
labelstring—Accessible name; renders the chart as a role="figure".
descriptionstring—Visually hidden summary for assistive tech.
loadingbooleanfalseRender the skeleton.
errorReactNode—Render an error message instead of the chart.
emptyStateReactNode"No data to display"Shown when data is empty.

Missing data

A null leaves a gap, and that is the default. A gap is information: it says nobody measured this point.

connectNulls bridges the gap by drawing a straight segment between the two readings that surround it. That segment is an interpolation — a value nobody recorded, drawn in the same stroke as the data. Turn it on only when a gap genuinely means "unchanged since the last reading".

example.tsx
// Honest: Wednesday is missing, and the chart says so.
<LineChart data={readings} config={config} categoryKey="day" />

// Only when a gap really means "no change".
<LineChart data={readings} config={config} categoryKey="day" connectNulls />

Choosing a y-axis

The axis is anchored at zero by default. autoMinValue fits it to the data instead.

A truncated y-axis is legitimate on a line chart. A line encodes change, and a 4% movement on a zero-anchored axis is a flat line that tells the reader nothing. This is exactly the opposite of a bar chart, where length encodes magnitude and cropping the axis misstates the data.

Use minValue / maxValue when a chart must stay comparable across reloads, or when the scale is externally defined (a 0–100 score, a target band).

Non-colour differentiation

Colour alone is unreliable — for colour-vision-deficient readers, in greyscale print, and on a bad projector. LineChart gives you three encodings to layer on top of hue:

  • Markers — dots="all" gives each line a dotted trail; dots="last" marks only the current reading.
  • Dashes — dashedSeries={["expenses"]} renders named series with a dash pattern. This is also the convention for a projection or a previous period.
  • Direct labels — showLabels="endpoints" or "extremes" prints values on the plot, so a series can be read without consulting the legend at all.
  • Sign — a signColored line is read against its zero line, its tooltip writes + or −, and dashedLoss dashes the part below zero.

Every series stroke is 2px, which is the minimum width for a chart mark to stay legible, and the --chart-* tokens are gated at 3:1 against the plot background.

Accessibility

VoiceOver + Safari: passed 2026-09-14. A static read gives "Visitors more than doubled between January and June, figure" and the summary. Tab lands on the plot, which VoiceOver reads after the figure name as "web application", and the arrow keys read each point through the tooltip ("Feb, Visitors, 1,450"). NVDA and JAWS are untested.

  • Name the insight, not the anatomy. label becomes the chart's accessible name and renders it as a role="figure". "Visitors doubled since March" is useful; "Line chart of visitors by month" repeats what the page already says.
  • description adds a visually hidden summary, so a static read carries the message without anyone having to interact with the plot.
  • accessibilityLayer is on: the plot is keyboard-navigable point by point, and the tooltip is a polite live region.
  • Animation respects prefers-reduced-motion: reduce.
  • With selectionMode="single" the periods become a list to choose from. The chart's one tab stop is a named listbox ("Visits this week: Periods"), each period an option with aria-selected and a name that carries its readings, and the period the arrow keys are on is its aria-activedescendant. Enter or Space chooses that period, so Enter no longer toggles the tooltip; Escape still hides it. A polite status region says "Tue selected" or "Selection cleared" once per change. The screen reader pass of this mode is still to be run.

Composition

children are rendered inside the chart, so anything Recharts draws is available without a prop for each one — ReferenceLine, ReferenceArea, ReferenceDot, Brush, LabelList.

It is also the escape hatch for a fully custom tooltip: switch the built-in one off and pass your own.

example.tsx
<LineChart data={data} config={config} categoryKey="month" showTooltip={false}>
  <ChartTooltip content={<MyTooltip />} />
  <ReferenceLine y={target} label="Target" />
</LineChart>

Boundaries

  • Range presets (1D/1W/1M/1Y), the big value callout, and a granularity switcher belong to the surrounding card, not to the chart. No surveyed design system ships them as chart props — see the "In a card" example.
  • A filled area under the line is AreaChart, not a prop here.
  • A single value against a target is Meter or Progress.
  • Categorical comparison is BarChart; a line implies a continuous axis.

Theming

ElementToken
Series stroke / marker--chart-1 … --chart-6 (via config[key].color)
Grid linesmuted-foreground at 20%
Axis tick labels and titlesmuted-foreground
Printed value labelsforeground; on a coloured plate, --surface-ink
Tooltip surfacepopover / popover-foreground / border

Migration

connectNulls now defaults to false. A chart whose data contains null previously drew straight through the gap; it now breaks the line there. Pass connectNulls to restore the old rendering — but read "Missing data" above first, because the bridged segment is a value nobody measured.

dir no longer defaults to "ltr". Unset, the chart reads the direction it inherits from the page, so it mirrors on an RTL page. Pass dir="ltr" to keep the old layout there.

showDots is deprecated in favour of dots. showDots keeps working for one minor and warns once in development; dots wins if both are set.

BeforeAfter
showDotsdots="all" (the default)
showDots={false}dots="none"
Bar ChartLive Line Chart

On this page

When to useWhen not to useInstallationUsageExamplesDefaultMultipleCurveSizesDots and labelsMissing dataY-axis domainAxesThresholds and bandsForecastPeriod comparisonProfit and lossSeries togglingSparklineIn a cardChoosing a dayOn a coloured plateStatesAPI ReferenceMissing dataChoosing a y-axisNon-colour differentiationAccessibilityCompositionBoundariesThemingMigration