# shadcn/ui Charts Explained: ChartConfig, Theming, and the Recharts 3 Upgrade

> 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.

Published: 2026-08-05 · Author: Matt Wierzbicki · Canonical: https://www.shadcndesign.com/blog/shadcn-ui-charts

shadcn/ui charts are not a charting library. They are a thin, themeable wrapper around [Recharts](https://recharts.org) — 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:** `ChartContainer` gives you a sized, styled Recharts `ResponsiveContainer` plus a `<style>` tag that turns your `ChartConfig` into `--color-*` CSS variables. Everything inside it — `<BarChart>`, `<Bar>`, `<XAxis>` — is plain Recharts. The chart component now ships on Recharts 3; if you upgrade, swap `hsl(var(--chart-1))` for `var(--chart-1)`, keep a height on `ChartContainer`, and stop relying on the tooltip to hold active state for you.

## What you actually install

```bash
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 inside
- `ChartTooltip` and `ChartTooltipContent`
- `ChartLegend` and `ChartLegendContent`
- The `ChartConfig` type

There is no `<Chart>` component, no chart type prop, no data schema. You compose Recharts primitives yourself. The [chart examples on ui.shadcn.com](https://ui.shadcn.com/charts) are copy-paste blocks — area, bar, line, pie, radar, radial — not an API surface.

## ChartConfig is a color and label registry

This is the piece that does the real work, and it is easy to mistake for decoration:

```tsx
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:

```css
[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:

```tsx
<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](/blog/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`:

```tsx
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>`.

## The five chart tokens

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:

```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);
}
```

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](/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, and the height problem

```tsx
<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.

## Tooltips and legends

`ChartTooltip` is a re-export of Recharts' `Tooltip`. All the shadcn/ui behavior lives in `ChartTooltipContent`, which you pass to its `content` prop:

```tsx
<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 `dataKey`s — 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 2 vs Recharts 3

[Recharts 3.0.0 shipped on 23 June 2025](https://github.com/recharts/recharts/releases/tag/v3.0.0), 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](https://ui.shadcn.com/docs/components/chart):

1. **CSS variable syntax.** Use `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.
2. **Tooltip state.** `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.
3. **`<Bar layout>`.** Drop the `layout` prop from `<Bar>` when the parent `<BarChart>` already declares it.
4. **Container sizing.** Keep a height, `min-h-*`, or `aspect-*` on `ChartContainer` so `ResponsiveContainer` can measure on first render.

### How to tell which one you have

`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:

```bash
# 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](/blog/shadcn-ui-registry-explained).

## Designing charts before you build them

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](/components/chart) 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.

## Frequently asked questions

**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.
