The shadcn Chart primitive is a thin theming and tooltip/legend layer over Recharts. It does NOT replace Recharts. You compose Recharts chart elements as children of ChartContainer, and the shadcn wrappers inject CSS variables (--color-<dataKey>) derived from a ChartConfig object so colors track the active theme (light / dark) without inline hex values.
Install with: npx shadcn@latest add chart. This creates components/ui/chart.tsx plus the dependency recharts in package.json.
Quick Reference
| Wrapper | Source | Role |
|---|---|---|
ChartContainer |
shadcn (custom) | Provides ChartConfig context, injects <style> with --color-<key> CSS vars, wraps Recharts ResponsiveContainer |
ChartTooltip |
re-export of Recharts.Tooltip |
The Recharts tooltip primitive, slotted via content prop |
ChartTooltipContent |
shadcn (custom) | Themed tooltip body. Props: indicator ("dot" | "line" | "dashed"), hideLabel, hideIndicator, labelKey, nameKey, labelFormatter, formatter |
ChartLegend |
re-export of Recharts.Legend |
The Recharts legend primitive, slotted via content prop |
ChartLegendContent |
shadcn (custom) | Themed legend body. Props: nameKey, hideIcon, verticalAlign |
ChartConfig (type) |
shadcn (custom) | Record<string, { label?: ReactNode; icon?: ComponentType } & ({ color?: string } | { theme: { light: string; dark: string } })> |
ALWAYS:
- Place
'use client'at the top of any file that imports a chart wrapper. Recharts usesResponsiveContainerwhich depends onResizeObserverand is non-serializable across the RSC boundary. - Wrap a Recharts chart (e.g.
BarChart,LineChart,PieChart) insideChartContainerand pass aconfigprop of typeChartConfig. - Set an explicit height contract on
ChartContainer: eitherclassName="min-h-[200px] w-full"orclassName="aspect-video". RechartsResponsiveContainermeasures parent dimensions and renders nothing when parent has zero height. - Reference colors in Recharts child props (e.g.
<Bar fill="var(--color-desktop)" />) usingvar(--color-<dataKey>)where<dataKey>matches a key inChartConfig. - Set chart-token CSS variables (
--chart-1through--chart-5) inglobals.cssunder:rootand.darksocolor: "var(--chart-1)"inChartConfigresolves per theme. The Companion Skillshadcn-core-themingdefines these tokens.
NEVER:
- Render a chart wrapper in a React Server Component without
'use client'. The build will fail withYou're importing a component that needs useContext. - Pass inline hex / hsl / oklch strings to
<Bar fill="#3b82f6" />or<Line stroke="#e11d48" />directly. This bypassesChartConfig, breaks dark mode, and loses theme synchronization. - Omit the
configprop onChartContainer. Without it, no--color-<dataKey>CSS vars are injected andvar(--color-desktop)resolves to nothing (Recharts falls back to default Recharts palette). - Use
Recharts.TooltipandRecharts.Legenddirectly. They render with default browser styling and ignore shadcn theme tokens. UseChartTooltip+ChartTooltipContentandChartLegend+ChartLegendContent. - Wrap
ChartContaineritself insideRecharts.ResponsiveContainer.ChartContaineralready includesResponsiveContainerinternally; double-wrapping produces zero-sized SVG.
Decision Tree: Which Chart Type?
Need to compare discrete categories? -> BarChart (Recharts.Bar)
Need to show trend over continuous axis? -> LineChart (Recharts.Line)
Need to show trend + magnitude/cumulative?-> AreaChart (Recharts.Area)
Need to show parts of a whole (<= 7 slices)? -> PieChart (Recharts.Pie)
Need to show multi-dimensional comparison? -> RadarChart (Recharts.Radar)
Need to show correlation between 2 metrics? -> ScatterChart (Recharts.Scatter)
Need to combine bars + lines + areas? -> ComposedChart (Recharts.ComposedChart)
Need radial / dial style? -> RadialBarChart (Recharts.RadialBar)
See references/methods.md for the per-chart-type Recharts integration table and references/examples.md for a complete code example of each.
ChartConfig Pattern
ChartConfig is the single source of truth for series labels, icons, and colors. The wrapper generates one --color-<key> CSS custom property per entry.
import { type ChartConfig } from "@/components/ui/chart"
const chartConfig = {
desktop: {
label: "Desktop",
color: "var(--chart-1)",
},
mobile: {
label: "Mobile",
color: "var(--chart-2)",
},
} satisfies ChartConfig
Two color modes are mutually exclusive per entry (the TypeScript discriminated union enforces this):
| Mode | Shape | Use when |
|---|---|---|
| Single color | { color: "var(--chart-1)" } |
Color is identical in light + dark (theme tokens already adapt) |
| Theme-split color | { theme: { light: "oklch(0.7 0.2 30)", dark: "oklch(0.8 0.15 30)" } } |
Light and dark need distinct, hand-picked colors that do NOT come from --chart-N tokens |
The wrapper injects:
[data-chart=chart-xyz] {
--color-desktop: var(--chart-1);
--color-mobile: var(--chart-2);
}
.dark [data-chart=chart-xyz] {
--color-desktop: var(--chart-1); /* same; --chart-1 itself is dark-aware */
--color-mobile: var(--chart-2);
}
Reference the keys in Recharts props: <Bar dataKey="desktop" fill="var(--color-desktop)" />. The dataKey matches the ChartConfig key, and the fill references the generated CSS var.
Color CSS-Var Convention
Two layers of CSS variables interact:
- Theme tokens (
--chart-1through--chart-5): defined inglobals.cssunder:rootand.dark. These are the brand palette. ALWAYS define exactly five (the official shadcn theme builder generates five). Owned by the theming skill. - Series tokens (
--color-<dataKey>): injected byChartContainerper chart instance, scoped to a[data-chart=<id>]selector. Owned byChartConfig.
The pattern: ChartConfig.color REFERENCES a theme token, then Recharts props reference the series token. This indirection lets one config drive many charts and lets one palette change cascade.
/* globals.css */
:root {
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
}
.dark {
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
}
Chart Type Catalog
| Chart type | Recharts root | Required children | Common series prop |
|---|---|---|---|
| Bar | BarChart |
XAxis, YAxis optional, CartesianGrid optional, Bar (one per series) |
dataKey, fill, radius, stackId |
| Line | LineChart |
XAxis, Line (one per series) |
dataKey, stroke, type (monotone | linear | step), dot |
| Area | AreaChart |
XAxis, Area (one per series), <defs> with <linearGradient> for fill gradient |
dataKey, stroke, fill, stackId, type |
| Pie | PieChart |
Pie (one), <Cell> array for per-slice colors |
dataKey, nameKey, innerRadius, outerRadius, paddingAngle |
| Radar | RadarChart |
PolarGrid, PolarAngleAxis, Radar |
dataKey, stroke, fill, fillOpacity |
| Scatter | ScatterChart |
XAxis, YAxis, Scatter |
dataKey, fill, shape |
| Composed | ComposedChart |
mix Bar, Line, Area |
per-child |
| RadialBar | RadialBarChart |
RadialBar |
dataKey, cornerRadius, background |
ALWAYS import the Recharts elements from "recharts" and the wrappers from "@/components/ui/chart". They live in separate import statements.
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
Theme-Aware Resolution
Color resolution is one-directional: Recharts prop -> --color-<dataKey> (per chart) -> ChartConfig.color -> --chart-N (theme) -> oklch value (light or dark scope).
When the user toggles the theme (via next-themes or any other mechanism that sets the .dark class on <html>), the theme tokens flip, the series tokens cascade automatically, and Recharts re-paints without re-mounting. No JavaScript code path is involved.
For per-instance overrides (e.g. one chart needs different colors than the shared palette), use the theme discriminator on the ChartConfig entry:
const chartConfig = {
revenue: {
label: "Revenue",
theme: {
light: "oklch(0.6 0.2 142)",
dark: "oklch(0.75 0.18 142)",
},
},
} satisfies ChartConfig
'use client' Requirement
EVERY file that imports any chart wrapper MUST declare 'use client' on line 1.
Reasons (all three apply):
ChartContainerusesReact.useId()andReact.useContext(ChartContext). Hooks are illegal in RSCs.- Recharts
ResponsiveContainerregisters aResizeObserveron the host element.ResizeObserverdoes not exist in the Node runtime that renders RSCs. - SVG
<defs>with<linearGradient>that Area charts use rely on stable client-sideidallocation.
Symptom of missing 'use client' in Next.js App Router: build-time error You're importing a component that needs useContext. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
Recovery pattern: extract the chart into a separate *.client.tsx file (e.g. revenue-chart.client.tsx) with 'use client' at the top, then import it from your server-rendered page. The page itself stays a Server Component; only the chart is a Client boundary.
Custom Tooltip Content
The default ChartTooltipContent renders an indicator + label + value triplet for each series. Customization happens via props:
| Prop | Type | Default | Use for |
|---|---|---|---|
indicator |
"dot" | "line" | "dashed" |
"dot" |
Visual marker style. "line" for line/area charts, "dot" for bars |
hideLabel |
boolean |
false |
Hide the X-axis label (top row of tooltip) |
hideIndicator |
boolean |
false |
Hide the colored marker entirely |
labelKey |
string |
dataKey of first item |
Override which field provides the label string |
nameKey |
string |
name of payload item |
Override which field provides the series name |
labelFormatter |
(label, payload) => ReactNode |
none | Custom label rendering (e.g. date formatting) |
formatter |
(value, name, item, index, payload) => ReactNode |
none | Full custom row rendering |
For total custom control, replace ChartTooltipContent with your own component passed to content. The child receives Recharts TooltipProps plus access to ChartConfig via useChart() (exported from @/components/ui/chart).
See references/examples.md for a custom-tooltip example.
Companion Skills
Load on demand when their topic surfaces during chart work:
- shadcn-core-theming: defines
--chart-1through--chart-5CSS variables, light/dark scoping, oklch color space. Required reading before customizing chart palette. - shadcn-impl-component-install: covers
npx shadcn@latest add chartworkflow, dependency resolution forrecharts, and post-install adjustments tocomponents/ui/chart.tsx. - shadcn-impl-rsc-vs-client-boundaries: covers when to split a page into server / client components. Charts ALWAYS fall on the client side; the boundary is typically the parent page.
- shadcn-core-stack: maps Recharts as the underlying visualization library and explains why shadcn wraps rather than reimplements.
Common Failure Modes
See references/anti-patterns.md for the exhaustive list. The top five:
ChartContainerwithoutChartConfig-> chart renders but with default Recharts colors, no theme awareness.- Inline
fill="#3b82f6"-> chart renders but dark mode shows wrong colors. - Missing
'use client'-> RSC build error. - Missing height constraint on
ChartContainer-> chart renders 0 pixels tall (silent). - Importing
Tooltip/Legendfromrechartsinstead ofChartTooltip/ChartLegendfrom@/components/ui/chart-> theming wrappers not applied.
Reference Files
references/methods.md: full wrapper signatures, ChartConfig type definition, per-chart-type Recharts integration table.references/examples.md: working code for BarChart (multi-series), LineChart (multi-series), stacked AreaChart, PieChart with legend, custom ChartTooltipContent override.references/anti-patterns.md: seven anti-patterns with symptoms, root cause, and fix.