Skip to main content

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 0 at the type level (yDomain?: [0, number | "auto"]), so a truncated-baseline area chart is unrepresentable.
  • Prefer LineChart when composition doesn't matter and you are comparing the shapes of trajectories.
  • Prefer BarChart when 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:

Area chart with 14 data points. Use the left and right arrow keys to move between data points, Page Up and Page Down to jump by a tenth of the data, Home and End to jump to the first and last, Enter or Space to activate the current point, and Escape to dismiss the readout.
import { AreaChart } from "@ngrok/mantle/area-chart"; const trafficData = [	{ date: new Date("2026-07-01"), http: 4200, tcp: 1400, tls: 860 },	{ date: new Date("2026-07-02"), http: 4600, tcp: 1350, tls: 900 },	{ date: new Date("2026-07-03"), http: 5100, tcp: 1500, tls: 940 },	{ date: new Date("2026-07-04"), http: 3800, tcp: 1250, tls: 780 },	{ date: new Date("2026-07-05"), http: 3500, tcp: 1200, tls: 750 },	{ date: new Date("2026-07-06"), http: 4900, tcp: 1550, tls: 980 },	{ date: new Date("2026-07-07"), http: 5400, tcp: 1600, tls: 1050 },	{ date: new Date("2026-07-08"), http: 5800, tcp: 1700, tls: 1100 },	{ date: new Date("2026-07-09"), http: 5200, tcp: 1650, tls: 1020 },	{ date: new Date("2026-07-10"), http: 6100, tcp: 1800, tls: 1150 },	{ date: new Date("2026-07-11"), http: 4400, tcp: 1450, tls: 880 },	{ date: new Date("2026-07-12"), http: 4100, tcp: 1400, tls: 840 },	{ date: new Date("2026-07-13"), http: 5600, tcp: 1750, tls: 1080 },	{ date: new Date("2026-07-14"), http: 6300, tcp: 1900, tls: 1200 },]; <AreaChart.Root	data={trafficData}	xKey="date"	stacked	aria-label="Connections by protocol"	className="max-w-2xl">	<AreaChart.Grid />	<AreaChart.XAxis />	<AreaChart.YAxis tickFormat={(value) => `${value / 1000}k`} />	<AreaChart.Area dataKey="http" label="HTTP" />	<AreaChart.Area dataKey="tcp" label="TCP" />	<AreaChart.Area dataKey="tls" label="TLS" />	<AreaChart.Tooltip />	<AreaChart.Legend /></AreaChart.Root>;

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.

Area chart with 14 data points. Use the left and right arrow keys to move between data points, Page Up and Page Down to jump by a tenth of the data, Home and End to jump to the first and last, Enter or Space to activate the current point, and Escape to dismiss the readout.
import { AreaChart } from "@ngrok/mantle/area-chart"; const regionData = [	{ date: new Date("2026-07-01"), usEast: 2400, euWest: 1900 },	{ date: new Date("2026-07-02"), usEast: 2600, euWest: 2100 },	{ date: new Date("2026-07-03"), usEast: 2500, euWest: 2350 },	{ date: new Date("2026-07-04"), usEast: 2100, euWest: 2500 },	{ date: new Date("2026-07-05"), usEast: 1950, euWest: 2650 },	{ date: new Date("2026-07-06"), usEast: 2300, euWest: 2600 },	{ date: new Date("2026-07-07"), usEast: 2700, euWest: 2450 },	{ date: new Date("2026-07-08"), usEast: 2900, euWest: 2300 },	{ date: new Date("2026-07-09"), usEast: 2800, euWest: 2500 },	{ date: new Date("2026-07-10"), usEast: 3000, euWest: 2700 },	{ date: new Date("2026-07-11"), usEast: 2600, euWest: 2850 },	{ date: new Date("2026-07-12"), usEast: 2450, euWest: 2900 },	{ date: new Date("2026-07-13"), usEast: 2750, euWest: 2700 },	{ date: new Date("2026-07-14"), usEast: 3100, euWest: 2550 },]; <AreaChart.Root data={regionData} xKey="date" aria-label="Requests by region" className="max-w-2xl">	<AreaChart.Grid />	<AreaChart.XAxis />	<AreaChart.YAxis />	<AreaChart.Area dataKey="usEast" label="US East" />	<AreaChart.Area dataKey="euWest" label="EU West" />	<AreaChart.Tooltip />	<AreaChart.Legend /></AreaChart.Root>;

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.

Area chart with 14 data points. Use the left and right arrow keys to move between data points, Page Up and Page Down to jump by a tenth of the data, Home and End to jump to the first and last, Enter or Space to activate the current point, and Escape to dismiss the readout.
import { AreaChart } from "@ngrok/mantle/area-chart";import { useState } from "react"; const shapedRegionData = [	{ date: new Date("2026-07-01"), usEast: 2400, euWest: 1900, apSouth: 1150 },	{ date: new Date("2026-07-02"), usEast: 2600, euWest: 2100, apSouth: 1300 },	{ date: new Date("2026-07-03"), usEast: 2500, euWest: 2350, apSouth: 1250 },	{ date: new Date("2026-07-04"), usEast: 2100, euWest: 2500, apSouth: 1400 },	{ date: new Date("2026-07-05"), usEast: 1950, euWest: 2650, apSouth: 1500 },	{ date: new Date("2026-07-06"), usEast: 2300, euWest: 2600, apSouth: 1450 },	{ date: new Date("2026-07-07"), usEast: 2700, euWest: 2450, apSouth: 1350 },	{ date: new Date("2026-07-08"), usEast: 2900, euWest: 2300, apSouth: 1500 },	{ date: new Date("2026-07-09"), usEast: 2800, euWest: 2500, apSouth: 1600 },	{ date: new Date("2026-07-10"), usEast: 3000, euWest: 2700, apSouth: 1550 },	{ date: new Date("2026-07-11"), usEast: 2600, euWest: 2850, apSouth: 1650 },	{ date: new Date("2026-07-12"), usEast: 2450, euWest: 2900, apSouth: 1600 },	{ date: new Date("2026-07-13"), usEast: 2750, euWest: 2700, apSouth: 1500 },	{ date: new Date("2026-07-14"), usEast: 3100, euWest: 2550, apSouth: 1700 },]; function ShapesExample() {	// Pin the active index so the hover dots — where an area chart wears its	// glyphs — stay visible; hovering still moves the crosshair freely.	const [activeIndex, setActiveIndex] = useState(9);	return (		<AreaChart.Root			data={shapedRegionData}			xKey="date"			stacked			activeIndex={activeIndex}			onActiveIndexChange={(index) => setActiveIndex(index ?? 9)}			aria-label="Requests by region, shape-coded"		>			<AreaChart.Grid />			<AreaChart.XAxis />			<AreaChart.YAxis />			<AreaChart.Area dataKey="usEast" label="US East" />			<AreaChart.Area dataKey="euWest" label="EU West" />			<AreaChart.Area dataKey="apSouth" label="AP South" shape="star" />			<AreaChart.Tooltip />			<AreaChart.Legend />		</AreaChart.Root>	);}

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)

Area chart with 7 data points. Use the left and right arrow keys to move between data points, Page Up and Page Down to jump by a tenth of the data, Home and End to jump to the first and last, Enter or Space to activate the current point, and Escape to dismiss the readout.

curve="monotone"

Area chart with 7 data points. Use the left and right arrow keys to move between data points, Page Up and Page Down to jump by a tenth of the data, Home and End to jump to the first and last, Enter or Space to activate the current point, and Escape to dismiss the readout.
import { AreaChart } from "@ngrok/mantle/area-chart"; const sessionData = [	{ date: new Date("2026-07-08"), sessions: 320 },	{ date: new Date("2026-07-09"), sessions: 180 },	{ date: new Date("2026-07-10"), sessions: 540 },	{ date: new Date("2026-07-11"), sessions: 260 },	{ date: new Date("2026-07-12"), sessions: 210 },	{ date: new Date("2026-07-13"), sessions: 480 },	{ date: new Date("2026-07-14"), sessions: 380 },]; <div className="grid w-full gap-6 sm:grid-cols-2">	<div className="flex flex-col gap-2">		<p id="area-curve-linear" className="text-muted font-mono text-xs">			curve="linear" (default)		</p>		<AreaChart.Root data={sessionData} xKey="date" aria-labelledby="area-curve-linear">			<AreaChart.Grid />			<AreaChart.XAxis />			<AreaChart.YAxis />			<AreaChart.Area dataKey="sessions" label="Sessions" />			<AreaChart.Tooltip />		</AreaChart.Root>	</div>	<div className="flex flex-col gap-2">		<p id="area-curve-monotone" className="text-muted font-mono text-xs">			curve="monotone"		</p>		<AreaChart.Root data={sessionData} xKey="date" aria-labelledby="area-curve-monotone">			<AreaChart.Grid />			<AreaChart.XAxis />			<AreaChart.YAxis />			<AreaChart.Area dataKey="sessions" label="Sessions" curve="monotone" />			<AreaChart.Tooltip />		</AreaChart.Root>	</div></div>;

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)

Area chart with 10 data points. Use the left and right arrow keys to move between data points, Page Up and Page Down to jump by a tenth of the data, Home and End to jump to the first and last, Enter or Space to activate the current point, and Escape to dismiss the readout.

connectNulls

Area chart with 10 data points. Use the left and right arrow keys to move between data points, Page Up and Page Down to jump by a tenth of the data, Home and End to jump to the first and last, Enter or Space to activate the current point, and Escape to dismiss the readout.
import { AreaChart } from "@ngrok/mantle/area-chart"; const outageData = [	{ date: new Date("2026-07-05"), requests: 1200 },	{ date: new Date("2026-07-06"), requests: 1350 },	{ date: new Date("2026-07-07"), requests: 1500 },	{ date: new Date("2026-07-08"), requests: null },	{ date: new Date("2026-07-09"), requests: null },	{ date: new Date("2026-07-10"), requests: 1420 },	{ date: new Date("2026-07-11"), requests: 1600 },	{ date: new Date("2026-07-12"), requests: 1680 },	{ date: new Date("2026-07-13"), requests: 1540 },	{ date: new Date("2026-07-14"), requests: 1750 },]; <div className="grid w-full gap-6 sm:grid-cols-2">	<div className="flex flex-col gap-2">		<p id="area-gaps-default" className="text-muted font-mono text-xs">			gaps (default)		</p>		<AreaChart.Root data={outageData} xKey="date" aria-labelledby="area-gaps-default">			<AreaChart.Grid />			<AreaChart.XAxis />			<AreaChart.YAxis />			<AreaChart.Area dataKey="requests" label="Requests" />			<AreaChart.Tooltip />		</AreaChart.Root>	</div>	<div className="flex flex-col gap-2">		<p id="area-gaps-connect" className="text-muted font-mono text-xs">			connectNulls		</p>		<AreaChart.Root data={outageData} xKey="date" aria-labelledby="area-gaps-connect">			<AreaChart.Grid />			<AreaChart.XAxis />			<AreaChart.YAxis />			<AreaChart.Area dataKey="requests" label="Requests" connectNulls />			<AreaChart.Tooltip />		</AreaChart.Root>	</div></div>;

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.

Area chart with 7 data points. Use the left and right arrow keys to move between data points, Page Up and Page Down to jump by a tenth of the data, Home and End to jump to the first and last, Enter or Space to activate the current point, and Escape to dismiss the readout.
import { AreaChart } from "@ngrok/mantle/area-chart"; const noTrafficYet = [	{ date: new Date("2026-07-08"), requests: 0 },	{ date: new Date("2026-07-09"), requests: 0 },	{ date: new Date("2026-07-10"), requests: 0 },	{ date: new Date("2026-07-11"), requests: 0 },	{ date: new Date("2026-07-12"), requests: 0 },	{ date: new Date("2026-07-13"), requests: 0 },	{ date: new Date("2026-07-14"), requests: 0 },]; function NoTrafficYetAreaChartExample() {	return (		<AreaChart.Root data={noTrafficYet} xKey="date" aria-label="Requests per day">			<AreaChart.Grid />			<AreaChart.XAxis />			<AreaChart.YAxis />			<AreaChart.Area dataKey="requests" label="Requests" />			<AreaChart.Tooltip />		</AreaChart.Root>	);}

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:

Area chart with 14 data points. Use the left and right arrow keys to move between data points, Page Up and Page Down to jump by a tenth of the data, Home and End to jump to the first and last, Enter or Space to activate the current point, and Escape to dismiss the readout.
import { AreaChart } from "@ngrok/mantle/area-chart"; const trafficData = [	{ date: new Date("2026-07-01"), http: 4200, tcp: 1400, tls: 860 },	{ date: new Date("2026-07-02"), http: 4600, tcp: 1350, tls: 900 },	{ date: new Date("2026-07-03"), http: 5100, tcp: 1500, tls: 940 },	{ date: new Date("2026-07-04"), http: 3800, tcp: 1250, tls: 780 },	{ date: new Date("2026-07-05"), http: 3500, tcp: 1200, tls: 750 },	{ date: new Date("2026-07-06"), http: 4900, tcp: 1550, tls: 980 },	{ date: new Date("2026-07-07"), http: 5400, tcp: 1600, tls: 1050 },	{ date: new Date("2026-07-08"), http: 5800, tcp: 1700, tls: 1100 },	{ date: new Date("2026-07-09"), http: 5200, tcp: 1650, tls: 1020 },	{ date: new Date("2026-07-10"), http: 6100, tcp: 1800, tls: 1150 },	{ date: new Date("2026-07-11"), http: 4400, tcp: 1450, tls: 880 },	{ date: new Date("2026-07-12"), http: 4100, tcp: 1400, tls: 840 },	{ date: new Date("2026-07-13"), http: 5600, tcp: 1750, tls: 1080 },	{ date: new Date("2026-07-14"), http: 6300, tcp: 1900, tls: 1200 },]; <AreaChart.Root	data={trafficData}	xKey="date"	stacked	aria-label="Connections by protocol against provisioned capacity"	className="max-w-2xl">	<AreaChart.Grid />	<AreaChart.XAxis />	<AreaChart.YAxis tickFormat={(value) => `${value / 1000}k`} />	<AreaChart.Area dataKey="http" label="HTTP" />	<AreaChart.Area dataKey="tcp" label="TCP" />	<AreaChart.Area dataKey="tls" label="TLS" />	<AreaChart.ReferenceLine y={9000} label="Capacity" />	<AreaChart.Tooltip />	<AreaChart.Legend /></AreaChart.Root>;

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.

Area chart with 60 data points. Use the left and right arrow keys to move between data points, Page Up and Page Down to jump by a tenth of the data, Home and End to jump to the first and last, Enter or Space to activate the current point, and Escape to dismiss the readout.
import { AreaChart } from "@ngrok/mantle/area-chart";import { useEffect, useState } from "react"; const streamEpoch = new Date("2026-07-14T12:00:00Z").getTime(); const formatClock = new Intl.DateTimeFormat("en-US", {	hour: "2-digit",	minute: "2-digit",	second: "2-digit",	hour12: false,	timeZone: "UTC",}); /** One deterministic point per tick: a long drift, a slow swell, and some chop. */function streamPoint(tick: number) {	const drift = 400 * Math.sin(tick / 240);	const swell = 600 * Math.sin(tick / 9);	const chop = 180 * Math.sin(tick / 2.2);	return {		time: new Date(streamEpoch + tick * 200),		throughput: Math.round(2400 + drift + swell + chop),	};} function LiveAreaExample() {	const [tick, setTick] = useState(0); 	useEffect(() => {		const interval = setInterval(() => {			setTick((current) => current + 1);		}, 200);		return () => clearInterval(interval);	}, []); 	// A sliding 60-point window: each tick appends one point and drops the oldest.	const data = Array.from({ length: 60 }, (_, index) => streamPoint(tick + index)); 	return (		<AreaChart.Root			data={data}			xKey="time"			aria-label="Live tunnel throughput"			className="max-w-2xl"		>			<AreaChart.Grid />			<AreaChart.XAxis				tickFormat={(value) => (value instanceof Date ? formatClock.format(value) : String(value))}			/>			<AreaChart.YAxis />			<AreaChart.Area dataKey="throughput" label="Throughput" curve="monotone" />			<AreaChart.Tooltip				labelFormat={(value) => (value instanceof Date ? formatClock.format(value) : String(value))}			/>		</AreaChart.Root>	);}

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.

Area chart with 14 data points. Use the left and right arrow keys to move between data points, Page Up and Page Down to jump by a tenth of the data, Home and End to jump to the first and last, Enter or Space to activate the current point, and Escape to dismiss the readout.
import { AreaChart } from "@ngrok/mantle/area-chart"; <AreaChart.Root data={trafficData} xKey="date" stacked aria-label="Traffic by protocol, copyable">	<AreaChart.Grid />	<AreaChart.XAxis />	<AreaChart.YAxis />	<AreaChart.Area dataKey="http" label="HTTP" />	<AreaChart.Area dataKey="tcp" label="TCP" />	<AreaChart.Area dataKey="tls" label="TLS" />	<AreaChart.Tooltip />	<AreaChart.Legend />	<AreaChart.CopyButton className="absolute right-0 top-0" /></AreaChart.Root>;

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.

modeUse it forA reader gets
"interactive" (default)A chart a reader explores: a dashboard panel, a detail page, a report with a question behind it.A named role="application" Tab stop, hover and keyboard point cursor, a tooltip readout, polite announcements, and an sr-only data table on focus.
"static"A chart a reader glances at while the exact values sit beside it: a sparkline in a table row or a card, a trend in a print report.A named role="img" with a text description (kind, count, range, series), no Tab stop, no hover, no tooltip, no table. animate defaults to false.
"decorative"A placeholder whose values are not information: synthetic an area behind an empty state, a hero backdrop.Nothing: hidden from assistive technology, out of the tab order, inert, every series in one neutral fill.

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.

<div className="flex items-center gap-2">	<AreaChart.Root		data={service.requests}		xKey="day"		mode="static"		aria-label={`Requests for ${service.name}, last 30 days`}		className="aspect-auto h-6 w-28"	>		<AreaChart.Area dataKey="count" label="Requests" />	</AreaChart.Root>	<span className="text-muted text-xs tabular-nums">{compact(service.requestTotal)}</span></div>

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.

import { Empty } from "@ngrok/mantle/empty";import { AreaChart } from "@ngrok/mantle/area-chart"; /** Synthetic area — a backdrop, not data — so no Math.random and no real values. */const placeholderTraffic = [30, 48, 40, 62, 55, 74, 66, 80].map((value, index) => ({	minute: index,	value,})); function EmptyStateAreaChartExample() {	return (		<div className="relative w-full">			<AreaChart.Root data={placeholderTraffic} xKey="minute" mode="decorative">				<AreaChart.Area dataKey="value" curve="monotone" />			</AreaChart.Root>			{/* The real message is a sibling of the chart, so AT still announces it. */}			<Empty.Root className="absolute inset-0 m-auto h-fit w-fit">				<Empty.Title>No traffic yet</Empty.Title>				<Empty.Description>					<p>Throughput will appear here once your endpoints start serving traffic.</p>				</Empty.Description>			</Empty.Root>		</div>	);}

Composition

Compose the parts of an AreaChart together to build your own:

AreaChart.Root├── AreaChart.Grid├── AreaChart.XAxis├── AreaChart.YAxis├── AreaChart.Area (one per series)├── AreaChart.ReferenceLine├── AreaChart.Tooltip├── AreaChart.Legend└── AreaChart.CopyButton
  • Root owns the data, the scales, the canvas renderer, and the entire interaction contract. Every other part must be composed inside it.
  • Area registers one series per part; with stacked on the Root, areas stack in registration order with the first-registered series at the baseline.
  • Grid, XAxis, YAxis, and ReferenceLine register chart chrome that the Root paints on canvas. Omit a part to omit that chrome.
  • Tooltip customizes the always-on hover/keyboard readout; omitting it never removes the hover layer.
  • Legend renders 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:

KeyAction
TabFocus the plot.
← / →Step between data points.
Page Up / Page DownStep by larger jumps.
Home / EndJump to the first / last data point.
Enter / SpaceActivate the current data point (fires onDatumActivate).
EscapeDismiss the point cursor and tooltip.

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:

PropTypeDefaultDescription
datareadonly TDatum[]Rows of data; each AreaChart.Area reads one numeric key per row.
xKeyExtract<keyof TDatum, string>The key of each row's x value.
aria-labelstringThe chart's accessible name. Exactly one of aria-label or aria-labelledby is required — enforced at the type level. Required unless mode is \"decorative\", which forbids it.
aria-labelledbystringThe id of a visible element that names the chart. Preferred over aria-label when the chart sits under a visible heading. Required unless mode is \"decorative\", which forbids it.
mode?"interactive" | "static" | "decorative""interactive"How much of the contract the Root exposes: explore it ("interactive"), glance at it with its values beside it ("static": a named role="img", no Tab stop, no tooltip, no table, animate off by default), or paint a placeholder ("decorative": hidden from assistive technology, one neutral fill). The name props are required unless decorative, and the interaction props are allowed only when interactive. See Modes.
xScale"time" | "linear" | "point"auto-detectedOverride the auto-detected x scale (Date values → "time", numbers → "linear", strings → "point").
yDomain[0, number | "auto"][0, "auto"]Override the automatic y maximum. The minimum is always the zero baseline.
stackedbooleanfalseStack the series in registration order, the first-registered series at the baseline.
animatebooleantrueAnimate enter/update transitions; the enter transition plays when the chart first comes into view. prefers-reduced-motion always wins.
pendingbooleanfalseHold the previous render at reduced opacity while fresh data loads — never swap a chart that has rendered once for a skeleton.
activeIndexnumber | nullControlled active datum index (null = none). Omit for uncontrolled hover. Allowed only when mode is "interactive".
onActiveIndexChange(index: number | null) => voidNotified when hover or keyboard stepping moves the active index. Allowed only when mode is "interactive".
onDatumActivate(event: ChartDatumEvent) => voidNotified on datum activation: a mark click, or Enter/Space on the focused chart. Allowed only when mode is "interactive".

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

Data AttributeValueDescription
data-slot"area-chart"The Root wrapper, with any forwarded chain ahead of it.
data-slot"area-chart-plot"The plot box. Every overlay layer positions against it, and the resize observer measures it.
data-slot"area-chart-canvas"The <canvas> the marks paint on. Always aria-hidden. Engine-owned: it is absent from server HTML and from a dormant chart, and the Root inserts it when the chart activates (near the viewport, on focus, or before print).
data-slot"area-chart-crosshair"The vertical hairline snapped to the active x. The engine writes its geometry as an inline style. Engine-owned: it is absent from server HTML and from a dormant chart, and the Root inserts it when the chart activates (near the viewport, on focus, or before print). Interactive charts only: a static or decorative chart inserts the canvas alone.
data-slot"area-chart-hover-band"The category highlight; an area chart never shows it. Engine-owned: it is absent from server HTML and from a dormant chart, and the Root inserts it when the chart activates (near the viewport, on focus, or before print). Interactive charts only: a static or decorative chart inserts the canvas alone.
data-slot"area-chart-markers"The container the engine syncs one hover dot per series into. Engine-owned: it is absent from server HTML and from a dormant chart, and the Root inserts it when the chart activates (near the viewport, on focus, or before print). Interactive charts only: a static or decorative chart inserts the canvas alone.
data-slot"area-chart-tooltip"The tooltip surface. The Root renders it on the first hover or keyboard step and keeps it; AreaChart.Tooltip configures it. A chart nobody points at holds none.
data-slot"area-chart-description"The sr-only description the overlay's aria-describedby points at: the chart kind, the point count, the x range, the series labels, and, on an interactive chart, the keyboard instructions. Omitted when mode="decorative".
data-slot"area-chart-data-table"The sr-only data table. Mounts once focus lands inside the chart (data-state flips to "engaged") and stays. Interactive charts only.
data-state"dormant" | "engaged"On the Root. Flips to "engaged" once focus lands inside the chart, and never flips back. The data table mounts with it. Visibility paints the chart but never flips it. Interactive charts only.
data-slot"chart-active-point"One hover dot inside the markers layer. Also carries data-shape.
data-shape"circle" | "square" | "triangle" | "diamond" | "triangle-down" | "plus" | "cross" | "star"On each hover dot, naming the glyph its series wears — the shape it set, else the one paired to its series slot. The dot clips itself to that glyph, so target this only to restyle a dot.

AreaChart.Area

One area series. Renderless — it registers the series with the chart, which paints it on canvas. Compose one Area per series.

PropTypeDefaultDescription
dataKeystringThe row key this series reads its numeric values from.
label?stringdataKeyDisplay name for the legend, tooltip, and data table.
seriesSlot?1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | "other"the next unreserved slotGive the series a fixed color-and-shape identity. See Chart color tokens.
color?"chart-1" … "chart-8" | "chart-other" | stringcolor from the resolved slotOverride only the color channel. This does not change the series slot or default shape. See Chart color tokens.
curve?"linear" | "monotone" | "step""linear"Interpolation between points: straight segments, smoothed without overshoot, or held steps.
connectNulls?booleanfalseJoin across null/missing values instead of leaving gaps. Gaps are the default and usually correct.
shape?"circle" | "square" | "triangle" | "diamond" | "triangle-down" | "plus" | "cross" | "star"the glyph paired to the series slotThe glyph the hover dot and legend key wear. This overrides only the shape channel. See Shapes.

AreaChart.Grid

Horizontal hairline gridlines behind the areas. Renderless — painted on canvas. Omit the part to omit the grid.

PropTypeDefaultDescription
lines"horizontal" | "vertical" | "both""horizontal"Which gridlines to draw.

AreaChart.XAxis

Tick labels along the bottom of the plot. Renderless — painted on canvas. Labels that would collide are skipped, never rotated.

PropTypeDefaultDescription
tickFormat(value: string | number | Date, context: { defaultLabel: string }) => stringFormat a tick label. Receives the row's x value: the category value on point scales, a Date on time scales, and a number on linear scales. The second argument is { defaultLabel }: the label the axis paints without a formatter.
tickCountnumberdensity-derivedTarget tick count (continuous scales only). Approximate: a linear axis over integer-only x values never renders fractional ticks.

AreaChart.YAxis

Value tick labels along the left of the plot. Renderless — painted on canvas. Ticks land on clean thousands-separated numbers.

PropTypeDefaultDescription
tickFormat(value: number, context: { defaultLabel: string; step: number }) => stringthousands-separatedFormat a tick label. The second argument carries the tick step and the defaultLabel the axis paints without a formatter, so a formatter can add a unit to defaultLabel and keep its precision.
tickCountnumber5Maximum tick count. When omitted, the axis aims for about 5 ticks. The axis can render fewer: ticks land on human-friendly steps, a vertical axis keeps about 28px for each tick, and a label that would overlap its neighbor is skipped. Integer-valued data never renders fractional ticks.

AreaChart.ReferenceLine

A dashed horizontal marker at a y value — an SLO threshold, a budget, a limit. Renderless — painted on canvas.

PropTypeDefaultDescription
ynumberThe y value the line marks.
labelstringA short label painted above the line's right end.
color"chart-1" … "chart-8" | "chart-other" | stringthe neutral reference colorLine color.

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:

PropTypeDefaultDescription
labelFormat(value: string | number | Date) => ReactNodeFormat the x/label row of the readout.
valueFormat(value: number, dataKey: string) => ReactNodeFormat each series value.
indicator"line" | "none""line"The per-series color key: a short stroke or nothing.
footerReactNode | (snapshot: HoverSnapshot) => ReactNodeExtra content appended below the series rows (e.g. a "view logs" hint).
children(snapshot: HoverSnapshot) => ReactNodeReplace the entire readout.

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

Data AttributeValueDescription
data-slot"area-chart-tooltip-label"On the <div> wrapping a children readout. The default readout has none.

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:

PropTypeDefaultDescription
children(series: SeriesMeta[]) => ReactNodeReplace the default legend items; receives the registered series.

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

Data AttributeValueDescription
data-slot"area-chart-legend"The legend list. AreaChart.Legend renders it in flow below the plot.
data-slot"area-chart-legend-label"On the <div> wrapping a children render prop. The default legend has none.
data-shape"circle" | "square" | "triangle" | "diamond" | "triangle-down" | "plus" | "cross" | "star"On each series' swatch, naming the glyph its AreaChart.Area wears. The swatch clips itself to that glyph, so target this only to restyle a key.

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:

PropTypeDefaultDescription
label?string"Copy data as Markdown"The accessible name, rendered as the button's aria-label.
onCopy?(value: string) => voidCallback fired after a successful copy, passes the serialized markdown.
onCopyError?(error: unknown) => voidCallback fired when an error occurs during copying.
asChild?booleanfalseCompose the copy behavior onto your own element instead of the default button.

While a copy is fresh, an always-mounted role="status" element beside the button announces "Copied" to a screen reader.

Data AttributeValueDescription
data-slot"area-chart-copy-button"The button the part renders.

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.

CSS VariableDescription
--color-chart-1The first categorical series slot (blue).
--color-chart-2The second categorical series slot (green).
--color-chart-3The third categorical series slot (pink).
--color-chart-4The fourth categorical series slot (red-orange).
--color-chart-5The fifth categorical series slot (teal).
--color-chart-6The sixth categorical series slot (magenta).
--color-chart-7The seventh categorical series slot (violet).
--color-chart-8The eighth categorical series slot (amber).
--color-chart-otherThe fold slot, worn once more than eight series show at once — group the tail into a single "Other" series.
--color-chart-decorativeThe neutral fill every series wears on a decorative chart, in place of its categorical slot.

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 seriesSlot on AreaChart.Area. It reserves one built-in color-and-shape pairing before automatic series take the remaining slots.
  • One color channel — pass color on AreaChart.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.

Area chart with 14 data points. Use the left and right arrow keys to move between data points, Page Up and Page Down to jump by a tenth of the data, Home and End to jump to the first and last, Enter or Space to activate the current point, and Escape to dismiss the readout.
import { AreaChart } from "@ngrok/mantle/area-chart"; const trafficData = [	{ date: new Date("2026-07-01"), http: 4200, tcp: 1400, tls: 860 },	{ date: new Date("2026-07-02"), http: 4600, tcp: 1350, tls: 900 },	{ date: new Date("2026-07-03"), http: 5100, tcp: 1500, tls: 940 },	{ date: new Date("2026-07-04"), http: 3800, tcp: 1250, tls: 780 },	{ date: new Date("2026-07-05"), http: 3500, tcp: 1200, tls: 750 },	{ date: new Date("2026-07-06"), http: 4900, tcp: 1550, tls: 980 },	{ date: new Date("2026-07-07"), http: 5400, tcp: 1600, tls: 1050 },	{ date: new Date("2026-07-08"), http: 5800, tcp: 1700, tls: 1100 },	{ date: new Date("2026-07-09"), http: 5200, tcp: 1650, tls: 1020 },	{ date: new Date("2026-07-10"), http: 6100, tcp: 1800, tls: 1150 },	{ date: new Date("2026-07-11"), http: 4400, tcp: 1450, tls: 880 },	{ date: new Date("2026-07-12"), http: 4100, tcp: 1400, tls: 840 },	{ date: new Date("2026-07-13"), http: 5600, tcp: 1750, tls: 1080 },	{ date: new Date("2026-07-14"), http: 6300, tcp: 1900, tls: 1200 },]; <AreaChart.Root	data={trafficData}	xKey="date"	stacked	// An app declares its brand token once, high in the tree. This example	// declares it beside the override so it stands alone. A theme that needs	// its own value takes a variant: `dark:[--color-brand:…]`.	className="max-w-2xl [--color-brand:#d07c00] [--color-chart-1:var(--color-brand)]"	aria-label="Connections by protocol, first slot rethemed">	<AreaChart.Grid />	<AreaChart.XAxis />	<AreaChart.YAxis tickFormat={(value) => `${value / 1000}k`} />	<AreaChart.Area dataKey="http" label="HTTP" />	<AreaChart.Area dataKey="tcp" label="TCP" />	<AreaChart.Area dataKey="tls" label="TLS" seriesSlot={4} color="var(--color-accent-600)" />	<AreaChart.Tooltip />	<AreaChart.Legend /></AreaChart.Root>;