One series filled down to zero, with gridlines, x labels, a scrubbable tooltip and an optional dashed line to compare against. Hand-rolled SVG on the free chart core (d3-scale and Motion), so paths are written to the DOM and nothing re-renders while you scrub or while it morphs.
When to use
- A single trend over time where the volume matters, such as signups per week or requests per day.
- This period against the last one, as a filled area and a dashed line on the same scale.
- A metric whose range or source changes in place: switching from 3 months to a year morphs the area instead of redrawing it.
When not to use
- A trend in a sentence or a table cell, with no axes: use a Sparkline.
- Three or more series, legend toggles, rolling values in the tooltip, a loading state, or several charts that change range together: use Line Chart (Pro).
- Counts per category rather than over time: use Bar Chart.
- Values that can go negative, such as profit and loss: an area filled to zero reads as volume, so use Line Chart (Pro).
Installation
npx shadcn add https://eased.dev/r/area-chart.jsonThe command adds Area Chart, everything it builds on and Eased’s tokens to your project.
Usage
import { AreaChart } from "@/components/eased/area-chart/area-chart";const weeks = [ { week: "2026-09-21", signups: 1184 }, { week: "2026-09-28", signups: 1243 }, { week: "2026-10-05", signups: 1310 }, { week: "2026-10-12", signups: 1276 },];const short = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric" });export function WeeklySignups() { return ( <AreaChart data={weeks} xKey="week" series={{ key: "signups", label: "Signups" }} label="Signups per week" format={(v) => v.toLocaleString("en-US")} formatX={(k) => short.format(new Date(`${k}T00:00`))} /> );}Examples
Against the previous period
Put both series on each datum and pass the second as compare. It draws as a dashed line at 40% in the same colour, with no fill, and a legend appears above the chart so the dash is never left to guess. Once shown, the legend row stays if compare is removed, so the plot doesn't jump while the dashed line flattens away.
const data = weeks.map((w, i) => ({ week: w.week, signups: w.value, previous: lastPeriod[i].value }));<AreaChart data={data} xKey="week" series={{ key: "signups", label: "This period" }} compare={{ key: "previous", label: "Previous period" }} label="Signups per week, against the 13 weeks before"/>A headline that follows the crosshair
onActiveChange reports the scrubbed point, then null when scrubbing stops. Show the point's value while it is set and the total otherwise; with Number Roll the digits roll between them.
const [active, setActive] = useState<number | null>(null);const total = data.reduce((sum, d) => sum + d.signups, 0);<NumberRoll value={active === null ? total : data[active].signups} /><AreaChart data={data} xKey="week" series={series} label="Signups per week" onActiveChange={(i) => setActive(i)} />API reference
AreaChart
| Prop | Type | Default | Description |
|---|---|---|---|
data | Record<string, string | number>[] | Points, oldest first, evenly spaced. | |
xKey | string | The field naming each point, such as an ISO date. Labels with the same key slide when the data changes. | |
series | { key: string; label: string } | The filled series: the field holding its values and its name in the tooltip, legend and hidden table. | |
compare | { key: string; label: string } | A dashed line to read against, such as the previous period. Adds a legend. | |
label | string | Names the figure, the slider and the hidden table, e.g. "Signups per week, last 13 weeks". | |
color | string | var(--eased-chart-1) | Any colour token, or chartColor(n). The comparison line uses it too. |
curve | "monotone" | "linear" | "monotone" | Monotone never overshoots a point, so a peak in the line is a peak in the data. |
height | number | 240 | The plot plus the x labels, in px. The legend sits above it. The width follows the container. |
format | (value: number) => string | one decimal | Values in the tooltip and for screen readers. |
formatTick | (value: number) => string | compact format | Gridline labels, e.g. 1.2K. |
formatX | (key: string) => string | the key | X axis labels. |
formatXDetail | (key: string) => string | formatX | The tooltip's heading and the screen reader's date, e.g. "Week of Oct 12, 2026". |
onActiveChange | (index: number | null, datum: D | null) => void | The scrubbed point, or null when scrubbing stops. | |
className | string | "" | Appended to the <figure>'s classes. |
The active dot is ringed in var(--eased-surface). On another background, set --chart-surface on a parent to that colour.
Keyboard
| Key | Action |
|---|---|
Tab | Focuses the plot and selects the newest point. |
← / → | Step one point back or forward (↓ / ↑ too). |
PageDown / PageUp | Jump a tenth of the points. |
Home / End | The first and the newest point. |
Escape | Lets go of the point. |
Accessibility
- The chart is a
<figure>named bylabel. The plot is onerole="slider"with the same name, and itsaria-valuetextreads the date and every value, such as "Week of Oct 12, 2026: This period 1,276, Previous period 1,102". - Every point is also in a visually hidden
<table>, so screen reader users can read the whole dataset in table navigation. - The SVG and the tooltip are
aria-hidden; the slider already says what they show. - Identity never rests on colour: the comparison is dashed and named in a legend.
- Touch scrubs the plot horizontally and still scrolls the page vertically.
Motion
The area draws in left to right once, the first time it scrolls into view, over 0.9s on the in-out curve, so it reads like a sentence. New data morphs from the shape on screen instead of redrawing: both series are resampled on shared x positions, so 13 points become 52, point for point, while the value scale and its gridlines slide on the same smooth spring (no overshoot, because data should never wobble past its value). X labels that stay slide to their new place; the rest fade. Scrubbing glides the crosshair and the dot on the snappy spring, and the tooltip follows on smooth, a beat behind, so it never jitters on a fast pointer. Reduced motion: no draw-in, no morph and no glide; the area, crosshair and tooltip move straight to their new place.
Notes for AI
Pick AreaChart for one metric over time, optionally against a previous period. Pass series and, for a comparison, compare, each as { key, label } naming fields on the same data. Give it a descriptive label, since that is what screen readers hear, and formatXDetail for a full date in the tooltip. For a headline above the chart, use onActiveChange rather than reading pointer events.
Don't remount it to change data (key={range}): pass the new data and it morphs. Don't wrap it in your own entrance animation, tooltip or ResizeObserver; it draws in, scrubs and sizes itself. Don't use it for more than two series or for negative values; reach for Line Chart (Pro).
Responsive
The chart fills its container's width and keeps its height. X labels thin to one per 76px, always keeping the newest, and the tooltip flips to the left of the point before it would leave the plot.
Related
- Sparkline: the same line at word size, without axes.
- Line Chart (Pro): several series, legend toggles that flatten a series, rolling tooltip values, a loading pulse and ranges shared across charts.
- Bar Chart: grouped or stacked bars per category.