Area Chart
betaFilled areas that show how amounts change over time, alone or stacked into a total, with a themed tooltip, legend, and states.
Preview
one
two
key on top
stacked
stacked, key on top
curve
texture
fill
start
line
intro
hover
idle
size
loads, then draws
Loading chart
loading
Loading chart
empty
No visitors yet
error
Visitors could not load.
Visitors could not load.
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/area-chart
Use it for
- Showing how an amount changes over time, when the size of the amount matters, like storage used.
- Use stacked to show how parts add up to a total over time, like visitors by device.
- Pass data, a config with one entry per area, and the categoryKey for the x axis.
- Pass valueFormat to format the tooltip and the y axis.
- Pass legend="top" for a key beside the title in place of the tooltip box. It shows the point under the pointer, and its numbers roll as you move. Press a series in the key to hide or show it.
- Dithered, the dots melt away toward the bottom and fade in from the axis, and hidden dots fill in near the pointer. Pass fill, start, line, intro, hover, or idle to change how they sit and move, or set them on a Dither around the chart.
Avoid it for
- Comparing trends that cross each other without stacking. Overlapping fills hide each other. Use LineChart.
- Categories with no order, like regions. Use BarChart.
- A small trend next to a number or in a table cell. Use Sparkline.
- More than five areas. The theme has five chart colors.
Props
- data
One object per point on the x axis, like { month: "Jan", desktop: 186 }.
TData[]
- config
The areas to draw, in order. Colors default to --chart-1 through --chart-5.
{ [key in keyof TData]?: { label: string; color?: string } }
- categoryKey
The field that names each point, shown on the x axis.
keyof TData
- title
A visible title. It names the chart. TypeScript requires title, aria-label, or aria-labelledby.
ReactNode
- aria-label
Names the chart when it has no visible title.
string
- size
Height: 5, 7, or 9 times --control-h-md, so density scales it.
"sm" | "md" | "lg" · default "md"
- stacked
Stacks the areas, so the top edge shows the total.
boolean · default false
- curve
How the top edge joins its points. Monotone is smooth and never overshoots a point.
"linear" | "monotone" | "step" · default "monotone"
- valueFormat
Formats tooltip values, and y axis ticks in short form, like 12.5K.
Intl.NumberFormatOptions
- locale
The locale for numbers. Fixed, so server and browser print the same text.
string · default "en-US"
- legend
Shows the legend. By default it shows when there are two or more areas. "top" shows it as a key beside the title, with the point under the pointer in place of the tooltip box, and the last point at rest.
boolean | "top"
- loading
Shows a soft area shape at the same height. When data arrives, it fades out while the areas draw in.
boolean · default false
- error
Shows this message in place of the chart. true shows a default message.
ReactNode
- onRetry
Shows a retry button under the error.
() => void
- emptyText
Shown when data is empty.
ReactNode · default "No data yet"
- texture
dither draws each area as dots that thin out toward the bottom and fade in from the axis, gradient fades it down from the top, and grain speckles it. The edge stays solid. Hover fills it in. Left out, the chart follows the Texture around it.
"flat" | "dither" | "gradient" | "grain"
- fill
Dithered, how the dots thin toward the bottom: one by one, or in 8 even steps. Left out, the chart follows the Dither around it.
"scatter" | "steps" · default "scatter"
- start
Dithered, how the areas start: fading in from the axis over 48 px, or 16 px in from it, then fading. Left out, the chart follows the Dither around it.
"fade" | "gap" · default "fade"
- line
Dithered, how each line starts: fading in over the same 48 px as the dots, or breaking into dots there. Left out, the chart follows the Dither around it.
"fade" | "dots" · default "fade"
- intro
Dithered, how the dots come in when the chart first shows. Motion off plays none. Left out, the chart follows the Dither around it.
"fade" | "sweep" | "rain" | "ripple" | "pop" | "fall" · default "fade"
- hover
Dithered, "fill" fills in hidden dots near the pointer, and they fade back when it leaves. Left out, the chart follows the Dither around it.
"fill" | "none" · default "fill"
- idle
Dithered, what the dots do at rest: a band of extra dots passing across, single dots blinking out and on, the thin edge filling in deeper and back, or soft patches of extra dots drifting across. Left out, the chart follows the Dither around it.
"none" | "wave" | "twinkle" | "breathe" | "drift" · default "none"
- className
Extra classes. Later classes win, so className='h-96' replaces the height.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| data | TData[] | - | One object per point on the x axis, like { month: "Jan", desktop: 186 }. |
| config | { [key in keyof TData]?: { label: string; color?: string } } | - | The areas to draw, in order. Colors default to --chart-1 through --chart-5. |
| categoryKey | keyof TData | - | The field that names each point, shown on the x axis. |
| title | ReactNode | - | A visible title. It names the chart. TypeScript requires title, aria-label, or aria-labelledby. |
| aria-label | string | - | Names the chart when it has no visible title. |
| size | "sm" | "md" | "lg" | "md" | Height: 5, 7, or 9 times --control-h-md, so density scales it. |
| stacked | boolean | false | Stacks the areas, so the top edge shows the total. |
| curve | "linear" | "monotone" | "step" | "monotone" | How the top edge joins its points. Monotone is smooth and never overshoots a point. |
| valueFormat | Intl.NumberFormatOptions | - | Formats tooltip values, and y axis ticks in short form, like 12.5K. |
| locale | string | "en-US" | The locale for numbers. Fixed, so server and browser print the same text. |
| legend | boolean | "top" | - | Shows the legend. By default it shows when there are two or more areas. "top" shows it as a key beside the title, with the point under the pointer in place of the tooltip box, and the last point at rest. |
| loading | boolean | false | Shows a soft area shape at the same height. When data arrives, it fades out while the areas draw in. |
| error | ReactNode | - | Shows this message in place of the chart. true shows a default message. |
| onRetry | () => void | - | Shows a retry button under the error. |
| emptyText | ReactNode | "No data yet" | Shown when data is empty. |
| texture | "flat" | "dither" | "gradient" | "grain" | - | dither draws each area as dots that thin out toward the bottom and fade in from the axis, gradient fades it down from the top, and grain speckles it. The edge stays solid. Hover fills it in. Left out, the chart follows the Texture around it. |
| fill | "scatter" | "steps" | "scatter" | Dithered, how the dots thin toward the bottom: one by one, or in 8 even steps. Left out, the chart follows the Dither around it. |
| start | "fade" | "gap" | "fade" | Dithered, how the areas start: fading in from the axis over 48 px, or 16 px in from it, then fading. Left out, the chart follows the Dither around it. |
| line | "fade" | "dots" | "fade" | Dithered, how each line starts: fading in over the same 48 px as the dots, or breaking into dots there. Left out, the chart follows the Dither around it. |
| intro | "fade" | "sweep" | "rain" | "ripple" | "pop" | "fall" | "fade" | Dithered, how the dots come in when the chart first shows. Motion off plays none. Left out, the chart follows the Dither around it. |
| hover | "fill" | "none" | "fill" | Dithered, "fill" fills in hidden dots near the pointer, and they fade back when it leaves. Left out, the chart follows the Dither around it. |
| idle | "none" | "wave" | "twinkle" | "breathe" | "drift" | "none" | Dithered, what the dots do at rest: a band of extra dots passing across, single dots blinking out and on, the thin edge filling in deeper and back, or soft patches of extra dots drifting across. Left out, the chart follows the Dither around it. |
| className | string | - | Extra classes. Later classes win, so className='h-96' replaces the height. |
Accessibility
- Renders a <figure> named by its title, or by aria-label. TypeScript requires one.
- Recharts' keyboard layer stays on: Tab focuses the chart, the arrow keys move between points, and the tooltip reads each one out. With the key on top, the key follows the arrow keys too, and the tooltip stays on the page for screen readers only, so each point is still read out.
- The key on top sits beside the title, not in it, so the chart's name never changes as the key moves. Each value holds the width of its longest one, so the key stays still as its numbers roll.
- Each series in the key is a toggle button. A hidden area fades out where it is, leaves the tooltip, and keeps the y axis still. In a stack, the rest glide down to close the gap once it has faded. The last one shown cannot hide. Tab reaches these buttons first, then the chart.
- Keeps one height while it loads, draws, fails, or has no data, so the page does not move. When the page prints, it asks the browser to keep a chart that fits on one page whole. In the light theme, it asks the browser to print legend dots and loading shapes even with backgrounds off.
- Loading, the error, and the empty text are read out through a status line that stays on the page.
- Each area's edge is a 2px line in --chart-1 to --chart-5, which the theme tests hold to 3:1 against the page. The fill under it fades out, so the edge carries the color.
- Areas draw in over three times --motion-duration-base, once the chart is on screen. They finish at once when the page prints. At 0ms they draw at once, and Recharts skips the animation for reduced motion.
- Dithered dots move only while the theme's pulse runs, so reduced motion and motion off keep them still. Forced colors keeps them still too, and shows every grid line. A print shows the still dots.
Examples
One area, with units
<AreaChart
title="Storage used"
data={[
{ day: "Mon", used: 42.5 },
{ day: "Tue", used: 44.1 },
{ day: "Wed", used: 47.8 },
]}
config={{ used: { label: "Used" } }}
categoryKey="day"
valueFormat={{ style: "unit", unit: "gigabyte", maximumFractionDigits: 1 }}
/><AreaChart
texture="dither"
title="Storage used"
data={[
{ day: "Mon", used: 42.5 },
{ day: "Tue", used: 44.1 },
{ day: "Wed", used: 47.8 },
]}
config={{ used: { label: "Used" } }}
categoryKey="day"
valueFormat={{ style: "unit", unit: "gigabyte", maximumFractionDigits: 1 }}
/><AreaChart
texture="gradient"
title="Storage used"
data={[
{ day: "Mon", used: 42.5 },
{ day: "Tue", used: 44.1 },
{ day: "Wed", used: 47.8 },
]}
config={{ used: { label: "Used" } }}
categoryKey="day"
valueFormat={{ style: "unit", unit: "gigabyte", maximumFractionDigits: 1 }}
/><AreaChart
texture="grain"
title="Storage used"
data={[
{ day: "Mon", used: 42.5 },
{ day: "Tue", used: 44.1 },
{ day: "Wed", used: 47.8 },
]}
config={{ used: { label: "Used" } }}
categoryKey="day"
valueFormat={{ style: "unit", unit: "gigabyte", maximumFractionDigits: 1 }}
/>Stacked
<AreaChart
title="All visitors"
data={[
{ month: "Jan", desktop: 186, mobile: 80 },
{ month: "Feb", desktop: 305, mobile: 200 },
{ month: "Mar", desktop: 237, mobile: 120 },
]}
config={{ desktop: { label: "Desktop" }, mobile: { label: "Mobile" } }}
categoryKey="month"
stacked
/><AreaChart
texture="dither"
title="All visitors"
data={[
{ month: "Jan", desktop: 186, mobile: 80 },
{ month: "Feb", desktop: 305, mobile: 200 },
{ month: "Mar", desktop: 237, mobile: 120 },
]}
config={{ desktop: { label: "Desktop" }, mobile: { label: "Mobile" } }}
categoryKey="month"
stacked
/><AreaChart
texture="gradient"
title="All visitors"
data={[
{ month: "Jan", desktop: 186, mobile: 80 },
{ month: "Feb", desktop: 305, mobile: 200 },
{ month: "Mar", desktop: 237, mobile: 120 },
]}
config={{ desktop: { label: "Desktop" }, mobile: { label: "Mobile" } }}
categoryKey="month"
stacked
/><AreaChart
texture="grain"
title="All visitors"
data={[
{ month: "Jan", desktop: 186, mobile: 80 },
{ month: "Feb", desktop: 305, mobile: 200 },
{ month: "Mar", desktop: 237, mobile: 120 },
]}
config={{ desktop: { label: "Desktop" }, mobile: { label: "Mobile" } }}
categoryKey="month"
stacked
/>Key beside the title in place of the tooltip
<AreaChart
title="Visitors by device"
data={[
{ month: "Jan", desktop: 186, mobile: 80 },
{ month: "Feb", desktop: 305, mobile: 200 },
{ month: "Mar", desktop: 237, mobile: 120 },
]}
config={{ desktop: { label: "Desktop" }, mobile: { label: "Mobile" } }}
categoryKey="month"
legend="top"
/><AreaChart
texture="dither"
title="Visitors by device"
data={[
{ month: "Jan", desktop: 186, mobile: 80 },
{ month: "Feb", desktop: 305, mobile: 200 },
{ month: "Mar", desktop: 237, mobile: 120 },
]}
config={{ desktop: { label: "Desktop" }, mobile: { label: "Mobile" } }}
categoryKey="month"
legend="top"
/><AreaChart
texture="gradient"
title="Visitors by device"
data={[
{ month: "Jan", desktop: 186, mobile: 80 },
{ month: "Feb", desktop: 305, mobile: 200 },
{ month: "Mar", desktop: 237, mobile: 120 },
]}
config={{ desktop: { label: "Desktop" }, mobile: { label: "Mobile" } }}
categoryKey="month"
legend="top"
/><AreaChart
texture="grain"
title="Visitors by device"
data={[
{ month: "Jan", desktop: 186, mobile: 80 },
{ month: "Feb", desktop: 305, mobile: 200 },
{ month: "Mar", desktop: 237, mobile: 120 },
]}
config={{ desktop: { label: "Desktop" }, mobile: { label: "Mobile" } }}
categoryKey="month"
legend="top"
/>While it loads, and when it fails
<AreaChart
aria-label="All visitors"
data={data ?? []}
config={{ desktop: { label: "Desktop" }, mobile: { label: "Mobile" } }}
categoryKey="month"
stacked
loading={isLoading}
error={error ? "Visitors could not load." : undefined}
onRetry={() => refetch()}
/><AreaChart
texture="dither"
aria-label="All visitors"
data={data ?? []}
config={{ desktop: { label: "Desktop" }, mobile: { label: "Mobile" } }}
categoryKey="month"
stacked
loading={isLoading}
error={error ? "Visitors could not load." : undefined}
onRetry={() => refetch()}
/><AreaChart
texture="gradient"
aria-label="All visitors"
data={data ?? []}
config={{ desktop: { label: "Desktop" }, mobile: { label: "Mobile" } }}
categoryKey="month"
stacked
loading={isLoading}
error={error ? "Visitors could not load." : undefined}
onRetry={() => refetch()}
/><AreaChart
texture="grain"
aria-label="All visitors"
data={data ?? []}
config={{ desktop: { label: "Desktop" }, mobile: { label: "Mobile" } }}
categoryKey="month"
stacked
loading={isLoading}
error={error ? "Visitors could not load." : undefined}
onRetry={() => refetch()}
/>