How shadcn/ui charts actually work: what ChartContainer does, how ChartConfig drives colors, and what breaks when you move from Recharts 2 to Recharts 3.

shadcn/ui charts are not a charting library. They are a thin, themeable wrapper around Recharts — about 350 lines of code that you copy into your project — and almost every problem people hit with them comes from not knowing where the wrapper ends and Recharts begins.
That boundary matters more than usual right now, because the chart component moved to Recharts 3 while a lot of existing projects are still on Recharts 2. The two versions look identical until a tooltip stops behaving or a chart renders at zero height.
TL;DR:
ChartContainergives you a sized, styled RechartsResponsiveContainerplus a<style>tag that turns yourChartConfiginto--color-*CSS variables. Everything inside it —<BarChart>,<Bar>,<XAxis>— is plain Recharts. The chart component now ships on Recharts 3; if you upgrade, swaphsl(var(--chart-1))forvar(--chart-1), keep a height onChartContainer, and stop relying on the tooltip to hold active state for you.
npx shadcn@latest add chart
That writes a single components/ui/chart.tsx into your project and installs its dependencies. The registry item is worth reading directly — https://ui.shadcn.com/r/styles/new-york-v4/chart.json ships exactly one file and pins recharts@3.8.0 plus lucide-react (checked 2026-08-05). A pinned major is a strong hint about how much the Recharts version matters here.
The file exports five things:
ChartContainer — the wrapper you put every chart insideChartTooltip and ChartTooltipContentChartLegend and ChartLegendContentChartConfig typeThere is no <Chart> component, no chart type prop, no data schema. You compose Recharts primitives yourself. The chart examples on ui.shadcn.com are copy-paste blocks — area, bar, line, pie, radar, radial — not an API surface.
This is the piece that does the real work, and it is easy to mistake for decoration:
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
The keys are not arbitrary. Each key maps to a dataKey in your data, and ChartContainer renders a <style> block scoped to that chart instance that declares one custom property per entry:
[data-chart=chart-r1] {
--color-desktop: var(--chart-1);
--color-mobile: var(--chart-2);
}
Which is why every shadcn/ui chart example refers to colors as var(--color-desktop) rather than the raw token:
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
The indirection is worth it. Your series colors get a semantic name once, in one object, and the chart, the tooltip, and the legend all read the same value. Change --chart-1 in your theme and every series bound to it moves together — the same pattern that makes the rest of shadcn/ui themeable, which we covered in how semantic colors work in shadcn/ui.
label feeds ChartTooltipContent and ChartLegendContent, so you get "Desktop" in the UI instead of the raw desktop key without writing a formatter. Config entries also accept an icon, and a theme object instead of color:
const chartConfig = {
desktop: {
label: "Desktop",
theme: { light: "var(--chart-1)", dark: "var(--chart-3)" },
},
} satisfies ChartConfig
With theme, the injected style block is emitted twice — once at the root and once under .dark — so a series can use a different hue per color scheme without any JavaScript. This is the one thing the wrapper gives you that Recharts genuinely cannot do on its own, because Recharts colors are props, and props do not respond to a class on <html>.
A default shadcn/ui theme defines five chart colors alongside the usual semantic ones. In a Tailwind v4 project they land in globals.css as oklch values:
: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);
}
Five is a design decision, not a technical ceiling. It is roughly the number of categorical series a reader can hold in their head at once, and it forces you to aggregate rather than plot fourteen lines. If you genuinely need more, add --chart-6 and up in the same block — nothing in chart.tsx enumerates them. The dark-mode block usually redefines all five at higher chroma, because the light-mode values are too muddy against a dark surface.
If you are building a palette rather than accepting the default, our theme generator emits the chart tokens along with the rest of the theme, so the series colors stay in the same system as your surfaces and text.
<ChartContainer config={chartConfig} className="min-h-[200px] w-full">
<BarChart accessibilityLayer data={chartData}>
<CartesianGrid vertical={false} />
<XAxis dataKey="month" tickLine={false} axisLine={false} />
<ChartTooltip content={<ChartTooltipContent />} />
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
</BarChart>
</ChartContainer>
ChartContainer renders a <div> with aspect-video, a stack of Tailwind selectors that restyle Recharts' internal SVG classes to your theme (axis ticks to muted-foreground, grid lines to border/50, focus outlines off), the generated <style> tag, and a Recharts ResponsiveContainer.
That ResponsiveContainer is where most "my chart is invisible" reports come from. It measures its parent and renders nothing meaningful if that parent has no resolvable height on first paint. The aspect-video default covers the common case, but the moment you put a chart inside a flex column, a grid row with 1fr, or a dialog that mounts hidden, you need to be explicit — a fixed height, a min-h-*, or an aspect-* on ChartContainer itself. Overriding the aspect ratio without supplying a height is the most common way to end up with a zero-height chart.
Note accessibilityLayer on the chart element: that is a Recharts prop, not a shadcn/ui one, and it enables keyboard navigation and screen-reader announcements for the series. It is in the official examples and worth keeping.
ChartTooltip is a re-export of Recharts' Tooltip. All the shadcn/ui behavior lives in ChartTooltipContent, which you pass to its content prop:
<ChartTooltip content={<ChartTooltipContent indicator="dashed" />} />
The props worth knowing:
| Prop | What it does |
|---|---|
indicator | "dot" (default), "line", or "dashed" swatch style |
hideLabel | Drops the header row — useful for single-series charts |
hideIndicator | Drops the color swatch |
nameKey | Reads the series name from a different key |
labelKey | Reads the tooltip header from a different key |
nameKey and labelKey exist for the case where your ChartConfig keys do not match your dataKeys — a pie chart keyed by browser where the config is keyed by browser name, for instance. Without them the tooltip falls back to dataKey, then name, then the literal string value, which is how you end up with a tooltip that says "value".
ChartLegendContent follows the same pattern and takes nameKey for the same reason.
Recharts 3.0.0 shipped on 23 June 2025, and the shadcn/ui chart component now targets it. The headline breaking changes in Recharts itself are about internals leaking out: CategoricalChartState is no longer handed to event handlers or <Customized />, and a batch of internal and previously-deprecated props were removed. If you never reached into Recharts' internal state, the upgrade is mostly uneventful.
The parts that touch shadcn/ui charts specifically, per the chart docs:
var(--chart-1), not hsl(var(--chart-1)). Older projects wrapped the token in hsl() because the tokens were bare HSL triplets; in a Tailwind v4 theme they are complete color values already, and double-wrapping produces an invalid color that silently renders as nothing.ChartTooltip.defaultIndex sets the initial tooltip only. If you were keeping a shape permanently active by driving the tooltip, hold that in your own component state now.<Bar layout>. Drop the layout prop from <Bar> when the parent <BarChart> already declares it.min-h-*, or aspect-* on ChartContainer so ResponsiveContainer can measure on first render.chart.tsx is copied into your repo, so the version in your project is whatever the CLI wrote the day you ran it — and it does not update itself. Two quick checks:
# What Recharts version is actually installed?
npm ls recharts
# Does your theme still double-wrap chart tokens?
grep -rn "hsl(var(--chart" app/ components/
If npm ls reports a 2.x and your globals.css still has hsl(var(--chart-1)), you are on the older pairing and consistent — that is a valid place to sit. The failure mode is mixing them: pulling a fresh Recharts 3 chart block into a project whose theme still wraps tokens in hsl(), or upgrading Recharts without re-adding chart.tsx. We hit exactly this in our own codebase, which still runs recharts@2.15.4 against a v2-era chart.tsx — internally consistent, and the reason a copy-pasted example from the current docs would not have themed correctly.
When you do upgrade, re-run npx shadcn@latest add chart and let it overwrite chart.tsx, then fix the call sites. Diff the file first if you have customized it; it is the same "you own the code" tradeoff that applies to every shadcn/ui component and to anything you install from a registry.
The wrapper solves theming, not composition. Deciding how many series belong on one chart, which axis to drop, what the empty and loading states look like, and how a dashboard's charts sit next to its cards and tables is still design work — and it is far cheaper in Figma than in Recharts props.
Our chart component in the shadcn/ui Figma kit mirrors this structure: the same five chart tokens as Figma variables, so a chart you lay out in Figma uses the identical palette the code will resolve at runtime, across all eight shadcn/ui styles.
Can I use a different charting library?
Yes, but not with this component. chart.tsx imports Recharts directly and its styling relies on Recharts' generated SVG class names. Swapping in a different library means writing your own wrapper.
Do shadcn/ui charts work with server components?
The chart file is marked "use client" and Recharts measures the DOM, so charts are client components. Fetch the data on the server and pass it down as props.
How do I add more than five series colors?
Define --chart-6, --chart-7, and so on in the same CSS block and reference them from ChartConfig. Nothing in the component caps the count. Consider whether the chart should be split instead.
Why does my tooltip say "value"?
The tooltip could not match your dataKey to a ChartConfig key. Either rename the config key to match the dataKey, or pass nameKey / labelKey to ChartTooltipContent.
Does npx shadcn@latest add chart update an existing chart.tsx?
It overwrites it, prompting first. Since the file lives in your repo, that is the only way to pick up upstream changes — including the Recharts 3 updates.
Founder @ shadcndesign.com

Learn how semantic colors work in shadcn/ui — why naming colors by role instead of hue keeps your UI consistent, accessible, and easy to theme across light and dark mode.

A step-by-step guide for pasting Tailwind v4 @theme CSS variables from a shadcn theme generator into your globals.css. Covers the new CSS-first theming workflow, common mistakes, and how to fix them fast.

A practical 2026 comparison of Figma design system kits that map cleanly to code, with notes on variables, plugins, maintenance, and pricing.