Area Chart
Token-driven area chart built on Recharts v3 with gradient fills, stacking, single and multi-series support, and full light/dark theming via chart tokens.
pnpm add @tessinaui/uiWhen to use
- The amount under the line matters. An area chart is a line chart filled down to its baseline, and the fill says this is an amount: revenue, traffic, storage used over time.
- Parts that add up to a total.
stackedshows how the series sum;expandturns each step into 100%, so the chart reads as share. - A range rather than a value. Pass
[low, high]pairs to draw a band, such as a normal range or a forecast envelope. - A trend inside a stat card. Switch the chrome off and the chart reads as a sparkline.
- A continuous x-axis. Dates and timestamps suit it; the fill assumes the points are joined.
When not to use
- When the shape of the line is the point. A trend where the amount does not matter, or whose axis should not start at zero, is a LineChart.
- To compare categories. Use a BarChart.
- For parts of one whole with no time axis. Use a PieChart.
- For several overlapping series. Unstacked fills cover each other; past two or three series, a LineChart reads better.
- When readers must read exact values. Stacked bands are hard to compare precisely; use a Table beside or instead of the chart.
- For a single data point. Show it as a number, not a chart.
- For an area read against bars or a line. A backlog behind weekly totals, or a forecast range behind a projection, is a ComposedChart.
Installation
pnpm add @tessinaui/ui rechartsUsage
import { AreaChart } from "@tessinaui/ui";
import type { ChartConfig } from "@tessinaui/ui";
const config: ChartConfig = {
desktop: { label: "Desktop", color: "var(--chart-1)" },
mobile: { label: "Mobile", color: "var(--chart-2)" },
};
const data = [
{ month: "Jan", desktop: 186, mobile: 80 },
{ month: "Feb", desktop: 305, mobile: 200 },
{ month: "Mar", desktop: 237, mobile: 120 },
];
<AreaChart data={data} config={config} categoryKey="month" />Series colours come from the --chart-1 … --chart-6 tokens, referenced as
var(--chart-N) in each config entry. They are brand-led, contrast-checked
in both modes, and recolour automatically when the theme flips — no JS re-render.
On a phone the legend and the axis take their height out of the chart's 2:1 box. size="lg" gives the plot more room, and startEndOnly keeps the x-axis to its first and last label.
Examples
Default
A single series with the softest natural curve and a brand-tone gradient fill.
Multiple Series
Two series in brand tones with a legend; the areas overlap rather than stack.
Stacked
Two series stacked into a cumulative area, with each solid contour separating the bands.
Expanded
Three series normalised to 0–100% proportional bands with expand.
Curve
linear joins points with straight segments; step holds each value until the next.
Axes
Both axes labelled, values formatted, and the chart given an accessible name and summary.
Reference lines and bands
Recharts children compose straight through: a shaded band for the weekend, a dashed goal line with its own label.
Range band
A [low, high] pair plots a band between two bounds — a normal range, a forecast envelope — with the actual reading drawn over it.
Missing data
A null leaves a gap. connectNulls bridges it with a straight segment interpolated between the surrounding readings — a value nobody measured, drawn in the same stroke as the data. Only correct when a gap genuinely means "unchanged".
Negative values
Values crossing zero, with a reference line marking the baseline.
Sparklines
Every piece of chrome turned off, so the chart reads as a trend line inside a stat card.
Interactive
A time-range control filtering the data, with a running total above the plot.
Choosing a month
With selectionMode="single" a click or a tap over a month, or Enter or Space
on the month the arrow keys are on, chooses it; choosing it again clears it.
The chosen period keeps the hover band, each area's dot on it wears a ring at the area's top, and its tick becomes the inverse
chip, so the choice reads without colour. Here the
readout above the chart follows the chosen month.
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 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. Keep text off the fill: a label drawn over an area sits
on a wash of the ink, which costs it contrast. The tooltip is a page panel, so
it keeps the page's look.
States
The loading skeleton and the empty state shown side by side.
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
data | AreaChartDatum[] | — | Row-per-category data. A value may be a [low, high] pair for a banded area. |
config | ChartConfig | — | Series metadata (label, color, icon). |
categoryKey | string | — | Datum key for the x-axis. |
series | string[] | all config keys | Which series to plot. |
curve | "monotone" | "natural" | "linear" | "step" | "monotone" | Interpolation. |
stacked | boolean | false | Stack series cumulatively. |
expand | boolean | false | Normalise the stack to 0–100% bands. Implies stacked. |
fill | "gradient" | "solid" | "gradient" | Fade toward the baseline, or a flat wash. |
fillOpacity | number | 0.25 | Top stop opacity of the fill. |
connectNulls | boolean | false | Bridge null gaps with an interpolated segment. |
size | "sm" | "md" | "lg" | "md" | Aspect ratio. |
showGrid / showXAxis / showLegend / showTooltip | boolean | true | Toggle chart chrome. |
showYAxis / showDots | boolean | false | Off by default. |
xAxisLabel / yAxisLabel | string | — | Axis titles. |
xTickFormatter / yTickFormatter | (value, index) => string | — | Format tick text. |
startEndOnly | boolean | false | Render only the first and last category ticks. |
dashedSeries | string[] | [] | Series keys rendered with a dashed stroke. |
label | string | — | Accessible name; renders the chart as a role="figure". |
description | string | — | Visually hidden summary for assistive tech. |
selectionMode | "none" | "single" | "none" | "single" lets a reader choose one period by click, tap, Enter or Space; choosing it again clears it. |
selected | string | null | — | The categoryKey value of the chosen period, controlled. null chooses nothing. |
defaultSelected | string | null | null | The 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". |
active | string | 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. |
strings | Partial<AreaChartStrings> | English, Arabic under dir="rtl" | Selection's words per entry: selected, cleared, items, period. |
children | ReactNode | — | Extra Recharts children — ReferenceLine, ReferenceArea, Brush. |
dir | "ltr" | "rtl" | inherited | Reading direction. Unset, read off the chart's position in the page once it mounts. |
loading | boolean | false | Render the skeleton. |
error | ReactNode | — | Render an error message instead of the chart. |
emptyState | ReactNode | "No data to display" | Shown when data is empty. |
initialDimension / debounce | — | {320,200} | Pre-measurement size and resize debounce, forwarded to Recharts. |
Theming
Every colour resolves to a Tessina token — nothing is hardcoded:
| Element | Token |
|---|---|
| Series fill / stroke | --chart-1 … --chart-6 (via config[key].color) |
| Grid lines | muted-foreground at 20% |
| Axis tick labels + axis titles | muted-foreground |
| Tooltip surface | popover / popover-foreground / border |
To re-skin a series, change the token reference in config (e.g. color: "var(--chart-3)")
or override the --chart-N token in your theme.
Accessibility
VoiceOver + Safari: passed 2026-09-14. A static read gives "Desktop visitors peaked in February, figure" and the axis labels. 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, Desktop, 305"). NVDA and JAWS are untested.
- Name the insight, not the axes.
labelrenders the chart as a namedrole="figure";descriptionadds a visually hidden summary. A static read has to carry the message — interaction is additive, never required. - Keyboard. Recharts'
accessibilityLayeris on by default: the chart surface is a tab stop,←/→step through data points,Enterpins the tooltip andEscdismisses it. There is no wrapping and noHome/End. The focus ring is drawn byChartContainer— Recharts ships none. See the Chart page for the full keyboard table and the VoiceOver QuickNav caveat. - Gaps are never interpolated.
connectNullsis off by default: a missing value stays a gap rather than being bridged. Turn it on only when a gap genuinely means "unchanged". - Colour is never the only differentiator. Pair multi-series with
dashedSeries, dots or direct labels, and keep to about five series — past that a stacked area stops being readable and a data table serves better. - With
selectionMode="single"the periods become a list to choose from. The chart's one tab stop is a namedlistbox("Traffic: Periods"), each period anoptionwitharia-selectedand a name that carries its readings, and the period the arrow keys are on is itsaria-activedescendant. Enter or Space chooses that period, so Enter no longer toggles the tooltip;Escapestill hides it. A polite status region says "Feb selected" or "Selection cleared" once per change. The screen reader pass of this mode is still to be run.