Chart
StableThemed, accessible charts built on Recharts, with a validated categorical palette, quiet chrome and tooltips that match the system.
Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/chart.jsonUsage
A chart is Recharts inside a ChartContainer. The container takes a config that names each series and assigns its color; every series color becomes a CSS variable, --color-<key>, scoped to that chart.
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const config = {
visitors: { label: "Visitors", color: "var(--chart-1)" },
} satisfies ChartConfig
<ChartContainer config={config} className="aspect-auto h-64 w-full">
<AreaChart data={data}>
<CartesianGrid vertical={false} />
<XAxis dataKey="day" tickLine={false} axisLine={false} />
<ChartTooltip content={<ChartTooltipContent indicator="line" />} />
<Area
dataKey="visitors"
stroke="var(--color-visitors)"
strokeWidth={2}
fill="var(--color-visitors)"
fillOpacity={0.1}
/>
</AreaChart>
</ChartContainer>Give the container an explicit height that includes the axis labels — aspect-auto h-64 — so the axes never get clipped into a nested scroll.
Examples
Area
A single series tells one story, so it needs no legend: the title names it. Draw a 2px line with a 10% wash of the same color underneath — a quiet fill, never a solid block.
Line
With two or more series, a legend is always present. Label the end of each line with its latest value instead of putting a number on every point.
Bar
Bars are at most 24px thick, grow from one baseline, and round only their data end (4px). A 2px gap separates bars that touch.
Stacked
For part-to-whole comparisons across many categories, stack horizontally. A 2px stroke in the surface color separates segments — the gap does the separating, not an outline.
Donut
Donuts show part-to-whole at a glance for up to six segments. For comparing close values, use a bar chart — angles are hard to compare.
Stat with sparkline
Often the right chart is a number. A stat tile leads with the value, states the change against a named period, and adds a 12-point sparkline: history in a quiet gray, the current period in the accent. Color the change by whether it's good news, and pair it with an arrow so direction never depends on color.
Emphasis
When the point is one series, say so: show it in the accent and everything else in gray. This is the most underused chart form, and often the honest answer to "make this chart clearer".
With a table view
Every chart should have a table twin. It's the most accessible form of the data, the one people copy from, and it keeps values reachable without hovering.
Color
Color comes last: pick the form first, then give each color exactly one job.
Categorical palette
chart-1chart-2chart-3chart-4chart-5The order — brand, teal, violet, orange, pink — isn't aesthetic. It was chosen by enumerating every ordering and keeping the one with the largest color difference between neighbors under simulated protanopia and deuteranopia. Every step also sits in the legible lightness band, clears the chroma floor, and keeps at least 3:1 contrast with the chart surface, in both modes:
Rules that follow from this:
- Assign in order, never skip, never cycle. The first series is always
chart-1. A sixth series folds into Other, becomes a small multiple, or moves to a table — never a generated sixth hue. - Color follows the entity, not its rank. When a filter removes a series, the others keep their colors.
- Scatter plots and maps get three series at most — there, any two colors can end up side by side.
- Status colors are reserved. Green and red mean good and bad everywhere in prfct; never use them for "series 4".
Guidelines
Choose the form first
Quiet chrome
Data is the only thing allowed to be loud. Gridlines are solid hairlines one step off the surface — never dashed. Axis labels use muted-foreground. Remove axis lines and tick marks; keep ticks at clean, comma-separated numbers.
One axis
Never plot two measures on two y-axes: the alignment of the scales is arbitrary, so the chart invents correlations. Use two charts, small multiples, or index both series to 100 at the start.
Labels and legends
- A legend is always present for two or more series; a single series is named by the title.
- Label selectively — the latest value, the extreme, the one series the story is about. Never a number on every point.
- Text wears text tokens. Values, labels and legends stay in
foregroundormuted-foreground; the colored mark beside them carries identity.
Accessibility
- Keyboard. Recharts' accessibility layer is on by default: focus a chart with Tab and move between data points with the arrow keys; the tooltip follows focus exactly as it follows the pointer.
- A text alternative. Pair every chart with a table view, or describe the takeaway in its title and caption — "Signups grew 64% since April" says more than "Signups by month".
- Never color alone. Legends and direct labels identify series; status changes pair color with an icon and words.
- Contrast. Every categorical color keeps at least 3:1 against the chart surface in both modes.
- Motion. Recharts animations default to
"auto", which respectsprefers-reduced-motion. Keep it.
| Key | Behavior |
|---|---|
Tab | Moves focus to the chart. |
←→ | Moves between data points and updates the tooltip. |
API reference
ChartContainer
Wraps a Recharts chart, scopes its colors and makes it responsive. Accepts all div props.
ChartConfig
type ChartConfig = Record<
string,
{
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string } // one color for both modes
| { theme: { light: string; dark: string } } // a color per mode
)
>ChartTooltipContent
Use as <ChartTooltip content={<ChartTooltipContent />} />.
ChartLegendContent
Use as <ChartLegend content={<ChartLegendContent />} />.
ChartStyle
Injected by ChartContainer; exported for custom containers that need the same scoped color variables.