Chart
A themed Recharts wrapper — token-only series colors, a bordered tooltip/legend, and Recharts' own built-in keyboard + screen-reader layer.
- Status
- Since
0.1.0- Accessibility pattern
- Recharts accessibility layer
Last updated
Install
Add Chart from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/chartIt also adds the sanctioned engine to your package.json: recharts (chart renderer engine).
Usage
import { CartesianGrid, Line, LineChart, XAxis } from "recharts";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart";
const chartConfig = {
desktop: { label: "Desktop", color: "chart-1" },
} satisfies ChartConfig;
const data = [
{ month: "Jan", desktop: 186 },
{ month: "Feb", desktop: 305 },
];
<ChartContainer config={chartConfig} className="h-56 w-full">
<LineChart accessibilityLayer data={data}>
<CartesianGrid vertical={false} stroke="var(--border)" />
<XAxis dataKey="month" tickLine={false} axisLine={false} />
<ChartTooltip content={<ChartTooltipContent />} />
<Line
dataKey="desktop"
stroke="var(--color-desktop)"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ChartContainer>;Theming
ChartConfig.color accepts only a chart-token reference: a bare token name ('chart-1' …
'chart-8') or the literal CSS var ('var(--chart-1)') — both resolve to the same
--chart-1…--chart-8 custom properties defined once in packages/design-tokens for :root and
.dark. There is no hex/oklch literal, and no { light, dark } theme pair.
const chartConfig = {
desktop: { label: "Desktop", color: "chart-1" },
mobile: { label: "Mobile", color: "var(--chart-2)" }, // equivalent form
} satisfies ChartConfig;ChartContainer reads config and sets one --color-<key> custom property per entry directly on its
own style (e.g. --color-desktop: var(--chart-1)) — reference it from any series:
stroke="var(--color-desktop)", fill="var(--color-desktop)".
This deliberately drops shadcn/ui's reference Chart's THEMES/ChartStyle mechanism (an
injected <style> tag keyed off a { light: '', dark: '.dark' } selector map, with per-config
{ light, dark } hex/oklch pairs). VegaStack's chart palette is already theme-split at the token
layer, so that machinery would only duplicate work the tokens already do — and per-config hex/oklch
literals are exactly what this repo's design-lint bans. A useful side effect: because the color is a
plain CSS var scoped by ordinary DOM ancestry (not a global id-keyed selector), there's no
React.useId()/data-chart id-scoping to manage either — multiple charts on one page just work.
Styling Recharts primitives directly
ChartContainer themes only what a generic sizing wrapper safely can for every chart shape: axis
tick label color and size (12px labels, mono 11px numerals), the plot surface's own focus ring, plus a few outline resets on Recharts' SVG layers. Grid-line, hover-cursor, and
point-dot colors are Recharts SVG presentation attributes baked into the library (CartesianGrid
defaults to stroke="#ccc", a Line's dot defaults to stroke="#fff", the BarChart hover cursor
defaults to stroke="#ccc") — set them directly, with tokens, on the primitive you compose:
<CartesianGrid stroke="var(--border)" vertical={false} />
<ChartTooltip cursor={{ stroke: "var(--border)" }} /> {/* Line/Area hover cursor */}
<ChartTooltip cursor={{ fill: "var(--muted)" }} /> {/* Bar hover cursor */}
<Line dot={{ fill: "var(--color-desktop)", r: 3 }} />Every example on this page does this. We don't replicate the shadcn reference's
[&_.recharts-x[stroke='#ccc']]:stroke-border/50-style attribute-value selector hack — it embeds hex
literals this repo's design-lint bans outright, and it silently stops matching the moment a future
Recharts version changes its internal default (verified against the installed recharts@3.10.1 source
while building this component).
Anatomy
Chart is not a compound wrapper around Recharts — it themes and sizes Recharts' own primitives,
which you import from recharts directly and compose as children:
<ChartContainer config={chartConfig}>
<BarChart data={data}>
{/* any recharts primitive: CartesianGrid, XAxis, YAxis, Bar, Line, Area, … */}
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
</BarChart>
</ChartContainer>ChartContainer— sizes the chart (ResponsiveContainer) and providesconfigvia context (data-slot="chart",<div>). Resolves eachconfigentry to a--color-<key>CSS var — see Theming below.ChartTooltip— Recharts'Tooltip, re-exported so you don't need a second import fromrecharts.ChartTooltipContent— the themedcontentforChartTooltip(data-slot="chart-tooltip-content",<div>): a borderedbg-popovercard with a label row and one row per series (color swatch, label,font-monovalue).ChartLegend— Recharts'Legend, re-exported for the same reason asChartTooltip.ChartLegendContent— the themedcontentforChartLegend(data-slot="chart-legend-content",<div>): a centered row of swatch/icon + label pairs.ChartConfig— the type forconfig: maps a data key to alabel, optionalicon, and a token-onlycolor— see Theming.
Examples
Area chart
Bar chart
Line chart
Pie chart
Use one config entry per slice and set each datum's fill to its scoped --color-<key> variable.
The adjacent text summary gives screen-reader users the same values without depending on SVG
support. Use pie/donut for a single part-to-whole snapshot, keep it to about five slices, and group
the remainder into “Other”; use a bar chart when close values need precise comparison.
Traffic sources: Organic 4,820 visitors, Paid 2,740, Referral 1,640.
Donut chart
The same data and semantic chart tokens can be presented as a donut by setting innerRadius and
outerRadius on Recharts' Pie primitive.
Traffic sources: Organic 4,820 visitors, Paid 2,740, Referral 1,640.
Tooltip indicator variants
ChartTooltipContent takes an indicator: 'dot' (default), 'line', or 'dashed'. This demo pins
each tooltip open at a fixed data point via Recharts' defaultIndex — a real, hover-free Recharts 3
prop, not a hack — so it stays deterministic under VRT.
dot
line
dashed
Legend
Pass a config entry's icon (any component, e.g. a lucide-react icon) to render it instead of the
default color swatch, in both the legend and the tooltip.
API Reference
ChartContainer
| Prop | Type | Default | Description |
|---|---|---|---|
children* | React.ReactNode | — | The Recharts chart element (e.g. <LineChart>…</LineChart>) — forwarded as ResponsiveContainer's
child (a plain element, not a render-prop — ResponsiveContainer's children is ReactNode in
this recharts version). |
config* | ChartConfig | — | Series/label/icon config — see ChartConfig. Drives each series' --color-<key> CSS var
and the label/icon lookups in ChartTooltipContent / ChartLegendContent. |
initialDimension | { width: number; height: number; } | { width: 320, height: 200 } | Size used for the very first render, before ResizeObserver reports the real parent box —
avoids a 0×0 flash on mount. |
Data attributes and CSS variables on ChartContainer
| Attribute | Values |
|---|---|
data-slot | "chart" |
ChartGrid
ChartGrid adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
Use ChartGrid instead of a raw CartesianGrid: it removes the vertical grid by default and applies
the design-system stroke token without falling back to Recharts' hardcoded color.
ChartTooltipContent
| Prop | Type | Default | Description |
|---|---|---|---|
hideIndicator | boolean | false | Hide the per-item color indicator swatch. |
hideLabel | boolean | false | Hide the label row above the item rows. |
indicator | "dashed" | "dot" | "line" | 'dot' | Shape of the per-item color swatch. |
labelKey | string | — | Data key to read the label from, when it differs from the payload's own label. |
nameKey | string | — | Data key to read the series name from, when it differs from the payload's own name/dataKey. |
Data attributes and CSS variables on ChartTooltipContent
| Attribute | Values |
|---|---|
data-slot | "chart-tooltip-content" | "chart-tooltip-indicator" |
--color-bg | CSS custom property |
--color-border | CSS custom property |
ChartLegendContent
| Prop | Type | Default | Description |
|---|---|---|---|
hideIcon | boolean | false | Hide each series' config.icon (when present) and always use the color swatch. |
nameKey | string | — | Data key to read the series name from, when it differs from the payload's own dataKey. |
Data attributes and CSS variables on ChartLegendContent
| Attribute | Values |
|---|---|
data-slot | "chart-legend-content" | "chart-legend-indicator" |
--color-bg | CSS custom property |
ChartConfig
ChartConfig is Record<string, { label?, icon?, color? }> (a mapped type, not a plain interface —
not auto-tabled). Per entry:
| Key | Type | Description |
|---|---|---|
label | React.ReactNode | Shown in the tooltip label row and the legend. |
icon | React.ComponentType | Replaces the color swatch in both the tooltip and the legend when set. |
color | ChartColorToken | 'var(--' + ChartColorToken + ')' | 'chart-1' … 'chart-8', or the literal var(--chart-N) form. Omit for an entry that only supplies a label/icon for lookup (e.g. an axis field). |
ChartColorToken is 'chart-1' | 'chart-2' | … | 'chart-8'.
Accessibility
Pass accessibilityLayer on the Recharts chart element (<LineChart accessibilityLayer>,
<BarChart accessibilityLayer>, …) — every example on this page does. Verified against the installed
recharts@3.10.1: when on, the chart's root <svg> gets role="application" and tabIndex={0}, and
Recharts' own accessibility layer wires arrow-key navigation between data points plus a live region
that announces the active point — a real interaction layer, not a static image. ChartContainer does
not add its own role="img", which would collide with Recharts' role="application"; the
accessible surface is Recharts' own. As of 3.10.1, accessibilityLayer already defaults to true
on every Cartesian/Polar chart — set it explicitly anyway, so it reads as intentional and survives a
future Recharts version changing that default.
| Key | Action |
|---|---|
| Tab | Move focus into the chart's <svg> (role="application"), which shows an inset focus ring. |
| ← / → | Move the active data point (Recharts' built-in accessibility layer). |
| Esc | Clear the active data point / close the tooltip. |
| Contract | States tested |
|---|---|
| Behaviour | default, active |
| Accessibility | native-or-base-ui-semantics, browser-accessibility-test |
| Visual | default, dark |