Skip to content
Folio

Charts

AreaChart

A filled series over a continuous axis, where the area means something.

Usage

When to reach for it

Reading one magnitude over time. Comparing several series against each other is a LineChart — four translucent fills stacked on each other answer neither question.

Ships from@misoto22/design/charts

TSX
import { AreaChart } from '@misoto22/design/charts'

Notes

A filled series over a category axis — the shape for a magnitude that is continuous, where the area under the line means something.

Composed rather than configured: axes, grid, tooltip, legend and the areas themselves are children, so a chart renders exactly the parts it asked for and nothing is switched on by a prop nobody can see.

Reach for <LineChart> instead when the reader is comparing several series against each other rather than reading one total, and for <BarChart> when the categories are discrete.

Anatomy

AreaChart anatomy
ElementDescription
Figure framerequiredThe <figure> ChartFigure draws, named through aria-labelledby rather than left to the figcaption, because deriving a name from a <figcaption> resolves in only some screen readers. The caption holds title and description together and is sr-only until showTitle is set, so a caveat written into description is announced and never printed.
PlotrequiredChartContainer: a 16:9 box floored at 13rem and capped at 26rem, and the one place Recharts’ hard-coded #ccc axis and grid strokes are re-pointed at --chart-grid and --chart-axis.
Areasrequired<AreaChart.Area>, one per series. Each generates its own id and scopes its gradient, its texture pattern and its reveal mask under it, so six variants share a plot without one overwriting another’s definitions.
Brush strip<AreaChart.Brush>, rendered in the container’s footer rather than inside the SVG. Both handles are role="slider" with aria-valuetext naming the row they sit on, so the window is reachable by arrow key.
Toolbar<AreaChart.Toolbar>, a role="group" row of at most five 44px icon buttons above the plot. Composing it also switches the plot’s own wheel, drag and keyboard zoom on, and the two drive one window rather than two.
Hidden data tableAn sr-only <table> built from the FULL data rather than the brushed window, so a reader on the table is never shown less than the CSV export holds. hideDataTable removes it, and zero rows render nothing.

Best practices

Do

  • Stack only quantities that genuinely add up. Under stackType="stacked" a band’s HEIGHT is its own value but its POSITION is the sum of everything under it, so stacking four independent rates draws a running total nobody measured.
  • Reach for stackType="expanded" when the reading is share rather than volume: it sets Recharts’ expand offset and <AreaChart.YAxis> swaps in percentTick on its own, so the axis reads 0% to 100% without a formatter at the call site. A tickFormatter of your own still wins — the axis defers to it rather than dropping it, which it used to do without a word.
  • Vary variant before the ramp on a two-area chart. The six fills are the primary encoding in the monochrome default, and under forced colours every --series-* token resolves to CanvasText — at which point the texture is the only thing left separating two areas.
  • Pass xDataKey. It is the rowKey of the hidden table, and without it the table renders no row-header column at all: a screen reader gets a column of numbers with no month beside them.

Don’t

  • Four translucent fills over each other is this form’s failure mode — the third area is read through two layers of --chart-fill and its own height stops being recoverable. Several series compared against each other is a LineChart, where nothing occludes anything.
  • connectNulls defaults to false for a reason: turned on, a gap in the data is drawn as a straight segment indistinguishable from a measured flat period. Set it only where the gap is a rendering artefact rather than a missing observation.
  • A single row draws nothing. One point has no segment to fill, dot is false unless <AreaChart.Dot> is composed, and the empty state does not fire because there IS a row — so the axes render over a blank plot.

Examples

default

Two series, two textures. In the monochrome default the fill is the primary thing that tells them apart and the grey ramp only supports it, which is also what survives a greyscale print and a colour-blind reader. Reach for an area rather than a line when the quantity under the curve is itself the reading.

Visitors per month
Visitors per month
monthDesktopMobile
Jan18680
Feb305200
Mar237120
Apr273190
May209130
Jun314240

fill variants

The six fills, cycled on one series so the differences are comparable. With no hue to spend, this is the axis that separates two areas before the ramp does, so a chart with more than one area should vary this before it varies anything else. gradient is the default and dissolves toward the axis, solid reads as a band rather than a slope, and the three textures are the ones that still separate two series in a greyscale print.

Visitors per month — gradient fill
Visitors per month — gradient fill
Desktop
186
305
237
273
209
314

stroke and curve

The two axes of the line itself, and they sit at different levels: strokeVariant belongs to one area, curveType belongs to the whole chart. animated-dashed is a SMIL loop, which the stylesheet's reduced-motion rule cannot reach, so the component gates it and drops it entirely rather than merely slowing it. The curve is the setting that changes what the picture claims rather than how it looks — step says nothing was measured between two rows, monotone says the quantity moved smoothly, and the rows cannot tell you which is true.

Visitors per month
Visitors per month
Desktop
186
305
237
273
209
314

stacking

Three different questions, not three looks: default compares two series, stacked reads their total, expanded reads their share — and the value axis switches to percentages on its own for the third. What stacking hides is the baseline: only the bottom band still sits on a straight line, so every band above it is a shape whose rises and falls are partly the band beneath it, which is why default is still the setting for "did mobile grow".

Visitors per month — stacked
Visitors per month — stacked
DesktopMobile
18680
305200
237120
273190
209130
314240

dots and reveal

Markers and the intro wipe are one subject because they share one mask: it covers the area's fill, its stroke and its resting dots together, so the markers arrive with the line instead of popping in ahead of it, which is what happens when the two are animated apart. The key on the chart is what replays the reveal — changing animationType alone would not remount the area. A reveal is a per-frame animated SVG mask and the heaviest thing in the package, which is why none is a real answer and is also where an OS reduce-motion preference lands.

Visitors per month
Visitors per month
Desktop
186
305
237
273
209
314

brush

Thirty-two days is past what one plot can show at once, and a brush is the answer that keeps the whole series visible while the reader picks a slice of it. It is a child rather than a showBrush prop, and both handles are real slider controls: tab to one and the arrow keys step it, Home and End jump it to the ends, where the shape this was ported from was pointer-only. The cost is a second miniature plot under the chart and the height it takes, so a chart of six rows does not want one.

Visitors per day
Visitors per day
dayDesktop
D1140
D2164
D3185
D4202
D5214
D6220
D7219
D8211
D9199
D10184
D11169
D12154
D13143
D14136
D15136
D16142
D17155
D18173
D19195
D20219
D21242
D22263
D23280
D24291
D25295
D26293
D27285
D28273
D29257
D30242
D31227
D32217

loading

One of the two states a real dashboard chart spends most of its life in, and the one that says "not yet" rather than "there is nothing here". The skeleton's rows are seeded from the row index rather than drawn from Math.random, so the server and the client render the same shape and a loading chart does not log a hydration mismatch — and it keeps the chart's measured height, so the page does not jump when the data lands. Under prefers-reduced-motion the travelling highlight is replaced by a flat wash.

Visitors per month
Loading

chroma palette

The sanctioned way to add hue, and the only one: the attribute on any ancestor re-points the eight series custom properties at a categorical palette validated against both grounds, and nothing in the chart below mentions a colour. The alternative is hand-picked hexes in a config's colors array, which is where an unvalidated, colour-blind-hostile palette gets born. Three of the light steps sit below 3:1 on paper, which is the documented reason the legend is not optional above one series.

Visitors per month
Visitors per month
monthDesktopMobileTablet
Jan1868040
Feb30520072
Mar23712055
Apr27319088
May20913061
Jun31424096

annotations

Three layers, drawn in the order editorial charting settled on and fixed by the package rather than by the order they are composed in: the band is context and sits behind the grid, the line is a claim about the data and sits above the marks, and the note explains both and sits above everything, the hover dot included. Most charts that look like they need a second series need a reference line instead — "are we above the line" is a question a threshold answers at a glance and a second series does not.

Visitors per month
Visitors per month
monthDesktop
Jan186
Feb305
Mar148
Apr162
May289
Jun341

empty

The other state a real dashboard reaches within a week — a filter that matches nothing — and the one worth writing a sentence for, because an empty pair of axes is indistinguishable from a chart that failed to load, so the reader reloads the page and it is still empty. Distinct from the loading skeleton: that one says "not yet", this one says "there is nothing here", and it usually has something to do next attached. Pass empty={false} instead to keep the bare axes, for a chart whose emptiness is itself the reading.

Visitors per month

No visits in this range

This project had no traffic before March. Try a wider range.

sonify

The hidden data table hands a screen-reader user every number, which is access but not a shape — six figures read one at a time do not say that March dipped, and a run of tones does, in under two seconds. The control announces the point count, the span of the category axis and the two extremes before the first note, so the melody is a measurement rather than a contour. Nothing ever plays until someone presses the button: there is no autoPlay prop and no effect that can begin a run.

Visitors per month
Visitors per month
monthDesktop
Jan186
Feb305
Mar148
Apr162
May289
Jun341

Types

TSX
export type ChartCurveType = ComponentProps<typeof RechartsArea>['type']
export type AreaVariant = 'gradient' | 'gradient-reverse' | 'solid' | 'dotted' | 'lines' | 'hatched'
export type AreaStrokeVariant = 'solid' | 'dashed' | 'animated-dashed'
export type AreaStackType = 'default' | 'stacked' | 'expanded'

Keyboard

AreaChart keyboard interactions
KeyDoes
TabReaches the plot, which Recharts’ accessibility layer makes navigable.
Moves the cursor between points, announcing each.

Accessibility

  • title is required and becomes the figure’s accessible name, printed or not.
  • The rows are rendered again as a visually hidden table, so the numbers are reachable rather than only drawn. hideDataTable opts out when the page already prints them.
  • Six fill variants exist because in the monochrome default TEXTURE is the primary carrier of identity and the grey ramp is the second — which is also what keeps two series apart in greyscale print and under forced colours.
  • The intro reveal is a per-frame SVG mask and is dropped entirely under prefers-reduced-motion, as is the crawling dash.