Chart
Token-themed chart primitives (container, config, tooltip, legend, data-table) composed with Recharts.
@garn/ui/chartOn this page
Default
Area chart with accessible name, gradient fills, tooltip, legend + a ChartDataTable.
Bar
Stacked bars with rounded tops.
Line
Multi-series line with active dots.
Pie
Donut with a token-tinted slice palette.
Interactive Legend
Click the legend to toggle series (useChartVisibility).
Reference Line
A dashed target line via ChartReferenceLine.
States
Loading (ChartSkeleton) + empty (Empty).
Formatting
Intl date/compact-currency axes + data labels.
Combo
Dual-axis composed bar + line.
Scatter
Two numeric axes; correlation.
Radar
Multi-axis comparison of two series.
Radial Bar
Concentric progress rings.
Funnel
Conversion funnel with stage labels.
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add chart- Registry
chart- Deps
recharts- Registry deps
utilsuse-prefers-reduced-motionskeleton
Import
import { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartDataTable, ChartReferenceLine, ChartReferenceArea, ChartLabelList, ChartGradient, ChartSkeleton, ChartStyle } from "@garn/ui/chart";Anatomy
The parts this component is built from — compose them to assemble it.
ChartContainerRequired[data-slot="chart"]Root; provides the config context + theming. Takes config and a single chart child. Defaults isAnimationActive onto the series inside (animate prop, off under prefers-reduced-motion); a series' own explicit value wins, and custom series read useChart().animate.
ChartTooltipInteraction primitive (the library Tooltip), re-exported.
ChartTooltipContent[data-slot="chart-tooltip-content"]Token-styled tooltip card: label + per-series swatch/value; indicator, hideLabel, nameKey, config-driven formatter.
ChartLegendInteraction primitive (the library Legend), re-exported.
ChartLegendContent[data-slot="chart-legend-content"]Token-styled legend; hidden + onItemClick make it an interactive show/hide toggle (consumer owns the state).
ChartDataTable[data-slot="chart-data-table"]The a11y data-table equivalent, generated from config + data (Intl numberFormat, collapsible).
ChartReferenceLineTarget/threshold line (aliased so the library detects it by type); token-styled by the container.
ChartReferenceAreaShaded reference band (aliased).
ChartLabelListData labels on marks (aliased); label text is token-styled.
ChartGradientA subtle same-hue fill gradient for areas/bars; put in <defs> with a unique id.
ChartSkeleton[data-slot="chart-skeleton"]A chart-shaped loading placeholder (composes Skeleton).
ChartStyleInternal — emits the scoped --color-<series> custom properties from config.
Props
The component's public props and their types.
animate- Description
- Entrance/update animation for the series inside. The container defaults `isAnimationActive` onto the library's series (and tooltip) so animation turns off automatically under `prefers-reduced-motion`; set `false` to force a static chart (screenshots, print). A series' own explicit `isAnimationActive` always wins over this default.
- Type
boolean- Default
true
childrenRequired- Description
- A single Recharts chart element (e.g. `<LineChart>…</LineChart>`).
- Type
React.ReactNode
configRequired- Description
- Series-key → label + token color map. Drives tooltip/legend + `--color-*`.
- Type
ChartConfig
Plus 279 inherited native <div> attributes.
Styling
Target these data-slots and states, and remap these tokens, to restyle without forking the component.
[data-slot="chart"]When to use
Build a real chart with axes, tooltip, and legend — line, area, bar, pie, scatter, radar, radial, combo, funnel.
Reach for something else when
- An inline trend with no chrome (use Sparkline)
- a single value vs a target (use Gauge or Bullet)
- a ranked list (use BarList).
Overview
Chart is not one chart — it's the themed primitives every chart is built from: a ChartContainer that turns a typed ChartConfig into scoped --color-<series> tokens and token-styles the SVG, plus a garn-styled tooltip, legend, data-table, reference lines, gradients, and loading/empty states. You compose these with the underlying library's chart elements; each chart type — line, area, bar, pie, scatter, radar, radial, combo, funnel — is a recipe, not a bespoke component. Recharts is the engine, added only when you copy a chart in (the sparkline and gauge stay engine-free).
Guidelines
Compose, don't configure. garn ships primitives + recipes, not a <LineChart data={} /> engine — you write the chart body in JSX and keep full control (mirrors the table decision). Copy a recipe and swap the data.
Theme through config. Point each series' color at a --garn-chart-* token; the container injects var(--color-<series>), so dark mode and re-theming come for free. Never hardcode a hex.
Own the accessibility. A chart needs an accessible name and a text/data-table equivalent (ChartDataTable gives you one for free), plus accessibilityLayer for keyboard + tooltip-on-focus. Distinguish series with more than colour.
Format for humans. Use numberFormatter/dateFormatter for axis ticks and tooltips, and a per-series config.formatter so values read as currency/percent/dates.
Let the container own motion. The entrance animation is JS-driven, so CSS motion-reduce can't stop it — ChartContainer does: it defaults isAnimationActive onto the series and turns animation off under prefers-reduced-motion. Set animate={false} for a static chart (screenshots, print, visual-regression suites); a per-series isAnimationActive overrides the default either way, and custom series read useChart().animate.
Best practices
- Drive series colours from
config→var(--color-<series>); enableaccessibilityLayer. - Pair every chart with an accessible name +
ChartDataTable.
- Don't hardcode colours or reach past the primitives into raw library styling.
- Don't ship a chart with no accessible name or data-table equivalent.
- Don't hand-wire
isAnimationActiveper series just to respect reduced motion — the container already does; reserve the per-series prop for real exceptions (e.g. the polar 0×0 gotcha).
Troubleshooting
Pie / radial / radar render nothing.
Cause. A Recharts v3 polar chart inside a responsive container that briefly measured 0×0 gets stuck.
Fix. Set isAnimationActive={false} on the Pie/RadialBar/Radar.
Tooltip/legend content is blank in tests.
Cause. The library reports 0×0 in headless/SSR test environments (no layout), so no payloads reach the content slots.
Fix. Verify those in the browser/lab; unit-test the engine-free parts (ChartDataTable, ChartStyle, hooks).
Gradient fill shows no colour.
Cause. The <linearGradient> id isn't unique / not referenced.
Fix. Give ChartGradient a useId() id and set fill={url(#${id})}.
useChart must be used within a <ChartContainer />.
Cause. A tooltip/legend content or hook rendered outside the container.
Fix. Keep them inside ChartContainer (which provides the config context).
Charts flake in screenshot/visual-regression runs (marks mid-entrance).
Cause. The ~1.5 s entrance animation is JS-driven and still running at capture time.
Fix. Set animate={false} on ChartContainer, or emulate prefers-reduced-motion: reduce in the test browser — both render the final frame immediately.
A custom series (or function-as-child chart) still animates under reduced motion.
Cause. The container's isAnimationActive defaulting only reaches the library's own elements at the chart's top level.
Fix. Read useChart().animate and pass it to your element's isAnimationActive yourself.
Accessibility
- Give the chart an accessible name (a `<figcaption>` + `aria-labelledby`, or `aria-label`).
- Provide a text/data-table equivalent — `ChartDataTable` generates one from the same config + data.
- Enable the library's `accessibilityLayer` for a single tab stop + arrow-key point navigation + tooltip-on-focus.
- Distinguish series without colour alone (markers / dashes / direct labels); series colours meet ≥3:1 in both themes.
- Series entrance animation is disabled automatically under `prefers-reduced-motion` (the container defaults `isAnimationActive` onto its series) — don't re-enable it per-series without cause.