Area Chart
A canvas-rendered area chart for showing how values — and, when stacked, a total and its parts — change over a continuous x axis. Each area fills with a 10% wash of its series color plus a 2px stroke on the band edge, rising from a shared zero baseline. Interaction ships with the Root unconditionally: a crosshair with an every-series tooltip, keyboard stepping with polite announcements, an sr-only description, and click / Enter / Space activation. The sr-only data table mounts once the chart takes focus. Large datasets decimate before painting.
When to use
Reach for an AreaChart when the magnitude of a value over time is the story: the fill gives the value physical weight. Add stacked and it becomes a part-to-whole chart over time: the top edge of the stack is the total, and each band shows how much of it a series contributes.
- The fill is a deliberate 10% wash with a 2px band-edge stroke, so an area chart reads as a line chart with weight, never a saturated block that hides the grid and its siblings.
- Areas always fill from a zero baseline: the y-domain minimum is fixed at
0at the type level (yDomain?: [0, number | "auto"]), so a truncated-baseline area chart is unrepresentable. - Prefer
LineChartwhen composition doesn't matter and you are comparing the shapes of trajectories. - Prefer
BarChartwhen the x axis is categorical and the comparison is between discrete things, not over time.
Keep it to eight series or fewer. The default chart tokens are eight validated categorical slots. The ninth series onward all wear the neutral --color-chart-other, so a dozen keys legend as a wall of identical gray. Eight is the palette's measured limit, not a style preference: all 28 pairs of the eight clear the colorblind-safe gates, and the closest pair clears its floor by 0.054 ΔE. A ninth hue has nowhere to sit. Rank by the window's total, keep the top seven, and sum the rest into one "Other" series. The Bar Chart page carries a worked fold. Set the fold's own Area to seriesSlot="other", so it reads as the tail rather than taking a categorical slot. Keep it last, so it sits at the top of a stack.
Compose AreaChart.Root with the chrome parts (Grid, XAxis, YAxis) and one AreaChart.Area per series. A stacked chart of connections by protocol over two weeks:
With stacked on the Root, series stack from the baseline up in registration order: areas that mount together register in composition order, so the first Area sits on the baseline and each later Area stacks on top of it. An Area that mounts later stacks on top of every series already registered, whatever its position in the JSX, and a dataKey that unmounts and returns resumes its original position. Reordering the parts of a mounted chart does not restack it: the position sticks to the dataKey, which is what keeps a series from jumping when you rename its label. The tooltip lists the top of the pile first, matching what a reader scans at the crosshair. Every multi-series chart must compose AreaChart.Legend — series identity must never rely on color-matching alone. (For a single series the legend renders nothing; the chart's title already names it.)
The tooltip, crosshair, and keyboard cursor are part of AreaChart.Root's interaction contract: composing AreaChart.Tooltip customizes the readout, and omitting it never removes the hover layer.
Overlapping areas
Without stacked, every series fills from the zero baseline and the areas overlap. The 10% wash is what keeps this legible: with two series you can read both bands and the region where they cross. Each series' hover dot also wears the glyph paired to its series slot (hover the crossing: US East wears circles, EU West squares), a redundant encoding alongside color for where the bands overlap; see Shapes below.
Overlap stops scaling: past two or three series the intersections turn to mud. When the series compose a meaningful total, stack them; when they don't, switch to a LineChart.
Shapes
An area chart wears its glyphs on the legend keys and the active-point dots: a filled band has no per-point mark, so the glyph shows in the legend and on the dots that ride the band edges at the crosshair.
Eight glyphs match the palette's eight series slots: "circle", "square", "triangle", "diamond", "triangle-down", "plus", "cross", "star", in that order. A series that sets no shape wears the glyph of its slot, so every band carries a second identity channel by default. That channel matters where color cannot carry identity alone: a reader with a color vision deficiency, a grayscale print or export, forced-colors mode. Set seriesSlot={3} for a fixed triangle identity.
Set shape to override the pairing; an explicit value always wins. The example below pins a controlled activeIndex (falling back to a fixed point when the pointer leaves) so the dots stay visible without hovering: US East and EU West take their slots' circle and square, and AP South pins the star over the triangle its slot would hand it. Move the crosshair and each dot keeps its series' glyph at every position. Point shapes on the scatter plot page is the full family treatment.
Curve interpolation
curve picks the interpolation between points: "linear" (the default) draws straight segments, "monotone" smooths the band without overshooting the data, and "step" holds each value until the next point. Smoothing is presentation only: the plotted values, tooltip readout, and keyboard cursor are identical in both charts below.
curve="linear" (default)
curve="monotone"
Gaps in the data
null (or missing) values render as gaps by default: a hole in the band that says "no data here", which is usually the honest choice for missed scrapes and collection outages. Set connectNulls on the Area to join across the hole when the gap is a collection artifact rather than a real absence.
gaps (default)
connectNulls
In a stacked chart a gap in one series contributes nothing to the stack at that x position, and the boundary stays continuous so the series above it don't tear.
No traffic yet
An all-zero series is data, not an empty state. Areas fill from the zero baseline, so the value axis stays
anchored there and the baseline sits on the axis rather than floating through the middle of the plot. Keep the
accessible name, the tooltip, and the data table: "0" is the answer, and a screen reader needs to hear it. Reach
for mode="decorative" only when the values are a synthetic backdrop behind an empty-state message.
Reference lines
AreaChart.ReferenceLine paints a dashed horizontal marker at a y value — an SLO threshold, a budget, a capacity limit — with an optional label above its right end. Here the stacked total crosses provisioned capacity twice:
Live data
For live charts, append points to the end of a sliding window and let the chart do the rest. When appends arrive at a steady rate, the x domain scrolls at one constant speed. The chart measures the time between appends and glides the domain to each new target over that interval, so the newest point slides in from the right edge. A point that arrives early or late changes the speed from the current position, with no jump. The plotted data itself never wobbles: new data must never make old data appear to move. As the window slides, axis tick labels (and the gridlines that share their fade) fade in and out instead of popping between tick sets, and the marks dissolve through a fade band at the plot's left edge (a scroll mask), so points exiting the window slide out smoothly instead of being clipped away. The mask activates automatically when the chart detects a sliding window and retires on its own once the stream idles. prefers-reduced-motion snaps everything instantly: no glide, no fades.
This demo appends one point every 200ms to a sliding 60-point window. Each point derives from a pure formula over a tick counter and a fixed epoch, so every render is deterministic; a real chart would append measurements as they arrive.
Copy as markdown
AreaChart.CopyButton copies the chart's current rows to the clipboard as a machine-stable, GitHub-flavored markdown table: one x column plus one column per series in paint order (ISO 8601 dates, plain un-separated numbers, an em dash for gaps), pasteable into Slack, issues, spreadsheets, and LLM chats. The button renders in flow where composed; Root is relatively positioned, so className can dock it over a corner.
Lists of charts
A page of 100 charts pays for the handful a reader can see. Every AreaChart.Root ingests its data and answers the keyboard from its first render, but it paints and allocates its canvas only once it comes within one viewport of the visible area or takes focus. Three viewports away it releases its canvas memory and repaints on the way back; printing paints every chart. The screen-reader data table waits for focus alone: it mounts once a reader tabs into the chart, so a page of sparklines holds no hidden tables until a reader asks for one. Add content-visibility: auto to your list's cards as a complement: it skips style and layout for offscreen cards, and the chart's own gate already skips paint and canvas memory.
Modes
mode sets how much of the chart's contract a Root exposes. Pick by what a reader does with the chart.
Noninteractive is not decorative. A sparkline beside a total still tells a reader when activity happened and how it varied, so it keeps a name and a description; only its values live in the text beside it. One hundred sparklines in a table as one hundred Tab stops is a focus order no reader wants, and a readout that only a pointer can reach fails WCAG 1.4.13, so "static" offers neither. A chart that is the only source of its values stays "interactive".
Static charts
Set mode="static" for a chart a reader glances at while the exact values sit beside it. The plot is a named role="img": it takes the same aria-label or aria-labelledby an interactive chart does, and its aria-describedby points at the description, so a screen reader hears the chart kind, the point count, the x range, and the series labels without a Tab stop. There is no interaction overlay: no hover band, no tooltip, no keyboard point cursor, no data table, and no live region, and a composed AreaChart.Tooltip has no effect. Pointer events pass through to the row or card behind it. The series keep their colors. animate defaults to false, because a list of sparklines that each play a reveal as the reader scrolls is motion with no information in it; pass animate to play the reveal. The interaction props (activeIndex, onActiveIndexChange, onDatumActivate) are forbidden at the type level. The Sparklines in a List recipe is the worked example.
Decorative charts
Set mode="decorative" to render the chart as a placeholder backdrop instead of real data: a synthetic area behind an empty state, an atmospheric hero graphic. A decorative chart keeps its visual rendering and animation but drops every layer that only makes sense for real data: it is hidden from assistive technology, removed from the tab order, and inert to pointer and keyboard, with no hover band, tooltip, sr-only data table, or live region. Because the values aren't information, mode="decorative" is mutually exclusive with the accessible-name props (aria-label / aria-labelledby) and the interaction props (activeIndex, onActiveIndexChange, onDatumActivate); the type won't compile with both.
A decorative chart also drops the categorical palette. Every series paints --color-chart-decorative, one neutral fill that outranks a series' color and seriesSlot. A backdrop carries no series identity, and a hue on values that are not information invites a reader to decode it.
That fill is what keeps an overlaid message readable. Empty.Description sits on text-muted, which measures 4.88:1 on a bare light card: 0.38 over the 4.5:1 floor for 14px text, so any tint under that copy spends the headroom. --color-chart-decorative is the darkest neutral step that still clears the floor in both themes: 4.56:1 in light and 4.93:1 in dark. The message needs no scrim behind it, no dimming, and no blur. Redeclare the token on the Root to retune the fill (className="[--color-chart-decorative:var(--color-neutral-100)]") and re-measure the copy against the value you pick.
Reach for mode="decorative" over hand-rolled inert + CSS: it removes the interaction overlay at the source, so consumers never target chart internals to neutralize it. Keep the real empty-state message as a sibling of the chart (not inside it) so screen readers still announce it; the decorative chart itself is silent.
mode="decorative" says the values are not information; it is not a disabled flag (a chart is not a form control). For real data a reader only glances at, use mode="static".
No traffic yet
Throughput will appear here once your endpoints start serving traffic.
Composition
Compose the parts of an AreaChart together to build your own:
Rootowns the data, the scales, the canvas renderer, and the entire interaction contract. Every other part must be composed inside it.Arearegisters one series per part; withstackedon the Root, areas stack in registration order with the first-registered series at the baseline.Grid,XAxis,YAxis, andReferenceLineregister chart chrome that the Root paints on canvas. Omit a part to omit that chrome.Tooltipcustomizes the always-on hover/keyboard readout; omitting it never removes the hover layer.Legendrenders series names keyed by color swatches as real DOM in flow below the plot. Always compose it on multi-series charts; it renders nothing for a single series.
Accessibility
Every interactive chart must have an accessible name: the Root requires exactly one of aria-label or aria-labelledby, enforced at the type level. Prefer aria-labelledby referencing a visible title when the chart sits under a heading — no drift-prone duplicated text. The name lands on the focusable interaction overlay, the chart's single named element (the canvas itself is hidden from assistive technology so the chart is never announced twice). A static chart names its plot role="img" instead and has no Tab stop. A decorative chart carries no real data, so it takes no accessible name and is hidden from assistive technology entirely.
The plot is a single tab stop with a keyboard point cursor:
Keyboard stepping announces the x value and every series' value at it through a debounced polite live region, so rapid arrowing never floods the screen-reader queue.
The overlay's aria-describedby points at a screen-reader description, data-slot="area-chart-description", so a screen reader hears what the chart holds when it takes focus: the chart kind, the number of data points, the x range from the first point to the last, the series labels, and the keyboard instructions. For example: "Area chart with 30 data points from Jul 1, 2026 to Jul 30, 2026. Series: Requests, Errors. Use the left and right arrow keys to move between data points, …". An empty chart reads "Area chart with no data." in place of the count and range. Server HTML carries the plain count alone. The range waits for hydration, because Intl output differs by locale, and the series labels register in layout effects, so both render on the client.
Every chart also renders an sr-only data table of the plotted data once it takes focus, bounded to the first 150 rows with a caption stating the summarization when the data is larger. An AreaChart.Root mounts dormant and engages once focus lands inside it (the overlay is the Tab stop, and a focusable part composed inside the Root, such as AreaChart.CopyButton, counts too), and it never goes dormant again. Visibility paints the chart but never mounts the table. The Root stamps data-state="dormant", then data-state="engaged", and the table mounts with the flip, so a screen reader's table list names only the charts the reader has focused. The description above names what every chart holds, focused or not. Tooltips enhance, they never gate: every plotted value is reachable through the keyboard point cursor or the table.
The table doubles as the chart's machine-readable surface: date row headers carry the exact ISO timestamp in <time dateTime> and value cells carry the raw number in data-value, so agents and scrapers read precise data from the DOM without reverse-engineering locale formatting. Assistive tech still hears the human-formatted text, and the table itself is addressable at data-slot="area-chart-data-table". To reach it, an agent focuses the chart (a scroll into view is not enough), then waits for data-state="engaged" on the Root. AreaChart.CopyButton hands over the same rows as markdown with neither step.
API Reference
AreaChart.Area, AreaChart.Grid, AreaChart.XAxis, AreaChart.YAxis, and AreaChart.ReferenceLine are renderless: they register configuration with the Root, which paints everything onto a single canvas. They accept no className, style, or other DOM props. AreaChart.Tooltip and AreaChart.Legend accept div props — the legend renders a real DOM element in flow, and the tooltip forwards its div props onto the tooltip element the Root renders. AreaChart.CopyButton also renders real DOM — an icon button accepting button props.
AreaChart.Root
The root of an area chart: owns the data, the scales, the canvas renderer, and the entire interaction contract. Renders a <div> wrapping the canvas plot and is generic over TDatum, the row type of data.
All props from div, plus:
ChartDatumEvent is importable from @ngrok/mantle/area-chart: { index: number; xValue: string | number | Date; datum: object; dataKey: string | null } (datum is the original row — narrow it to your row type), where dataKey names the series nearest the pointer on mouse activation and is null for keyboard activation.
Data attributes
AreaChart.Area
One area series. Renderless — it registers the series with the chart, which paints it on canvas. Compose one Area per series.
AreaChart.Grid
Horizontal hairline gridlines behind the areas. Renderless — painted on canvas. Omit the part to omit the grid.
AreaChart.XAxis
Tick labels along the bottom of the plot. Renderless — painted on canvas. Labels that would collide are skipped, never rotated.
AreaChart.YAxis
Value tick labels along the left of the plot. Renderless — painted on canvas. Ticks land on clean thousands-separated numbers.
AreaChart.ReferenceLine
A dashed horizontal marker at a y value — an SLO threshold, a budget, a limit. Renderless — painted on canvas.
AreaChart.Tooltip
Customizes the hover/keyboard tooltip readout. The tooltip itself is part of AreaChart.Root's interaction contract and renders even when this part is absent — compose it to format the label or values, append a footer, or replace the content entirely.
A children readout renders inside a <div data-slot="area-chart-tooltip-label">. When it goes away, React removes that div instead of your own text nodes: a browser translation engine reparents text nodes, and a removal aimed at one throws. The div is display: contents, so it adds no box and your content stays a direct layout child of the parent. To style it as a box, set a display of your own on it first. Browser translation covers the mechanism and the rules your own components need.
All props from div (forwarded onto the tooltip element the Root renders) except children, plus:
The HoverSnapshot passed to footer and children describes the active x position: { index, xValue, datum, points, viaKeyboard }, where points holds every series' value at that position. Stacked, the rows run from the top of the pile back to the baseline; unstacked, they follow registration order.
Data attributes
AreaChart.Legend
The legend: series names keyed by swatches carrying each series' color and glyph, rendered as real DOM in flow below the plot. Always compose it on multi-series charts — identity must never rely on color-matching alone. It renders nothing for a single series (the chart's title already names it). The legend is at most 64px tall (max-h-16) and scrolls past that, so many series never take the plot's height in a fixed-height Root. Set a max-h-* class in className to change the bound.
A children render prop renders inside a <div data-slot="area-chart-legend-label">. When it goes away, React removes that div instead of your own text nodes: a browser translation engine reparents text nodes, and a removal aimed at one throws. The div is display: contents, so it adds no box and your content stays a direct layout child of the parent. To style it as a box, set a display of your own on it first. Browser translation covers the mechanism and the rules your own components need.
All props from div except children, plus:
Each SeriesMeta is { dataKey, label, mark, color, colorInput, shape, texture }, where color is the CSS color string the series paints with and shape is the resolved glyph — the shape the series set, else the one paired to its series slot.
Data attributes
AreaChart.CopyButton
A button that copies the chart's current data to the clipboard as a GitHub-flavored markdown table: one x column plus one column per series in paint order. Values are machine-stable — ISO 8601 dates, plain un-separated numbers, an em dash for gaps — so the paste target receives parseable data, not locale formatting. Renders an icon button in flow where composed; Root is relatively positioned, so className can dock it over a corner (e.g. absolute right-0 top-0).
All props from button, plus:
While a copy is fresh, an always-mounted role="status" element beside the button announces "Copied" to a screen reader.
Chart color tokens
The ordered --color-chart-1 … --color-chart-8 tokens define the built-in series identities. Each of the two themes (light and dark) authors its own eight oklch() literals, measured so all 28 pairs hold 15 ΔE apart for full-color vision and 8 ΔE under simulated protanopia or deuteranopia, and so every slot clears 3:1 against that theme's chart surface.
Series receive unreserved slots in composition order. Set seriesSlot to give a series a fixed visual identity. Mantle reserves fixed slots before automatic series take the remaining slots. Removing an automatic series closes the gap. The color and shape props override only their own channels and never change slot assignment. seriesSlot="other" selects the neutral tail treatment without reserving a categorical slot. When the chart runs out of unreserved categorical slots, later automatic series paint with --color-chart-other. Fold the long tail into a single "Other" series rather than inventing more hues.
The Bar Chart page records what each theme measures over those 28 pairs.
Custom color strings are an escape hatch, not a shortcut: a raw hex value does not adapt across themes, and any custom palette must be re-validated for surface contrast and for colorblind-safe separation from every other color on the chart. A series that carries good or bad meaning — an error rate, a success rate — should wear the semantic status colors, never a categorical slot.
Series identity and color
Three public paths control a series identity or color.
- Fixed identity — pass
seriesSlotonAreaChart.Area. It reserves one built-in color-and-shape pairing before automatic series take the remaining slots. - One color channel — pass
coloronAreaChart.Area. It takes a chart token by name or any CSS color string without changing the slot or default shape. - One chart — redeclare the token on
AreaChart.Root:className="[--color-chart-1:var(--color-brand)]". Every series that lands in slot 1 picks the new value up. So do its legend key, band fill, and hover dot.
The chart resolves every token from Root's computed style in one read, inside that Root's own cascade, so a declaration scoped to one Root wins for that chart alone. Sibling charts on the page keep the theme value. A bare currentColor resolves from Root's computed color. A value the canvas cannot parse by itself, such as light-dark() or a currentColor nested inside color-mix(), resolves through a hidden element inside Root instead. Declare the same custom property higher up (on a dashboard wrapper, or at :root) to retheme a whole subtree.
An overridden color leaves mantle's gates behind, so measure the value you set against the slots its neighbors hold. The #d07c00 below shares a band edge with chart-2, and it clears 8 ΔE from that green under simulated protanopia and deuteranopia in both themes. TLS fixes slot 4 for a diamond identity, then overrides only its paint.