Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Chart

A themed Recharts wrapper — token-only series colors, a bordered tooltip/legend, and Recharts' own built-in keyboard + screen-reader layer.

Status
stable
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/chart

It 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 — data-slot="chart"
ChartGrid
ChartLegendContent — data-slot="chart-legend-content" | "chart-legend-indicator"
ChartTooltipContent — data-slot="chart-tooltip-content" | "chart-tooltip-indicator"
<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 provides config via context (data-slot="chart", <div>). Resolves each config entry to a --color-<key> CSS var — see Theming below.
  • ChartTooltip — Recharts' Tooltip, re-exported so you don't need a second import from recharts.
  • ChartTooltipContent — the themed content for ChartTooltip (data-slot="chart-tooltip-content", <div>): a bordered bg-popover card with a label row and one row per series (color swatch, label, font-mono value).
  • ChartLegend — Recharts' Legend, re-exported for the same reason as ChartTooltip.
  • ChartLegendContent — the themed content for ChartLegend (data-slot="chart-legend-content", <div>): a centered row of swatch/icon + label pairs.
  • ChartConfig — the type for config: maps a data key to a label, optional icon, and a token-only color — 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

PropTypeDefaultDescription
children*React.ReactNodeThe 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*ChartConfigSeries/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

AttributeValues
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

PropTypeDefaultDescription
hideIndicatorbooleanfalseHide the per-item color indicator swatch.
hideLabelbooleanfalseHide the label row above the item rows.
indicator"dashed" | "dot" | "line"'dot'Shape of the per-item color swatch.
labelKeystringData key to read the label from, when it differs from the payload's own label.
nameKeystringData key to read the series name from, when it differs from the payload's own name/dataKey.

Data attributes and CSS variables on ChartTooltipContent

AttributeValues
data-slot"chart-tooltip-content" | "chart-tooltip-indicator"
--color-bgCSS custom property
--color-borderCSS custom property

ChartLegendContent

PropTypeDefaultDescription
hideIconbooleanfalseHide each series' config.icon (when present) and always use the color swatch.
nameKeystringData key to read the series name from, when it differs from the payload's own dataKey.

Data attributes and CSS variables on ChartLegendContent

AttributeValues
data-slot"chart-legend-content" | "chart-legend-indicator"
--color-bgCSS custom property

ChartConfig

ChartConfig is Record<string, { label?, icon?, color? }> (a mapped type, not a plain interface — not auto-tabled). Per entry:

KeyTypeDescription
labelReact.ReactNodeShown in the tooltip label row and the legend.
iconReact.ComponentTypeReplaces the color swatch in both the tooltip and the legend when set.
colorChartColorToken | '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.

KeyAction
TabMove focus into the chart's <svg> (role="application"), which shows an inset focus ring.
/ Move the active data point (Recharts' built-in accessibility layer).
EscClear the active data point / close the tooltip.
ContractStates tested
Behaviourdefault, active
Accessibilitynative-or-base-ui-semantics, browser-accessibility-test
Visualdefault, dark

Do / Don't

Do
Set ChartConfig.color to a chart token ('chart-1' … 'chart-8') and reference it as var(--color-<key>) on your series — it stays correct in both themes automatically.
Don't
Hardcode a hex/oklch color on a series, or reach for a { light, dark } config pair — there is no theme-map escape hatch, and design-lint rejects the literal anyway.

On this page