Skip to content
Data Display

Chart

A themed wrapper around Recharts. You still write Recharts — the container adds the config context, generates a CSS variable per series, and restyles the grid, axes, and tooltip to match the rest of the set.

Import

import {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  ChartLegend,
  ChartLegendContent,
} from "@mikenotthepope/substrateui"

Composition

Everything inside the container is Recharts' own — the chart type, the axes, the grid. Only the tooltip and legend content are replaced, because those are the parts that carry the theme. ChartTooltip is Recharts' Tooltipre-exported, so it is the content prop that does the work.

ChartContainer
└── BarChart (any Recharts chart)
    ├── CartesianGrid
    ├── XAxis / YAxis
    ├── ChartTooltip
    │   └── ChartTooltipContent
    ├── ChartLegend
    │   └── ChartLegendContent
    └── Bar / Line / Area

The config is the colour source

Each key in config becomes a CSS variable named --color-<key>, scoped to that one chart. So a series keyed desktop is painted with fill="var(--color-desktop)" — the indirection is the point: the tooltip, the legend, and the bar all read one value, and changing the theme changes all three.

Point the config at the theme's own --chart-1 through --chart-5 rather than at a literal colour. Those five are defined per theme, light and dark, so a chart follows the palette instead of pinning it. Use theme instead of color where a series needs different values in light and dark.

satisfies ChartConfig rather than a type annotation, so the key names stay literal and --color-desktop is checkable.

const config = {
  // One value for both modes — a theme token already handles the switch.
  desktop: { label: "Desktop", color: "var(--chart-1)" },

  // Or split it explicitly when the series needs different colours per mode.
  mobile: { label: "Mobile", theme: { light: "var(--chart-2)", dark: "var(--chart-4)" } },
} satisfies ChartConfig

Lines and areas

The container makes no assumption about chart type — the same config and the same variables drive a line or an area chart. Note the legend, which reads its names from the config too.

Formatting the tooltip

formatter and labelFormatter reshape the rows and the heading. A number with no unit is a number the reader has to guess at, so this is usually worth setting rather than optional.

Animation

Recharts animates a series in from zero, and it does not check prefers-reduced-motion. Every example on this page passes isAnimationActive={false}, which is also what a screenshot test needs — an animating chart is a flaky snapshot.

Watch for a chart that renders its grid and axes but no data: a bar mid-animation has zero height, and a Recharts rectangle of zero height draws nothing. If the animation never completes, the chart stays empty and looks like a data problem rather than a motion one.

Sizing

The container is aspect-videoby default and wraps Recharts' ResponsiveContainer, which measures its parent. Give the container a height — or let the aspect ratio give it one — because a ResponsiveContainer in a parent of zero height measures zero and draws nothing.

Direction

Recharts draws into SVG with computed x-coordinates and does not read direction, so an RTL locale gets the axis still running left to right. Pass reversed on the XAxis and set the YAxis to orientation="right" where the chart should read the way the page does.

The tooltip and legend are ordinary DOM, so their text and layout flip on their own. This is the one place in the set where the direction toggle is not enough by itself.

Accessibility

A chart is a picture of data, and the picture is not the data. Recharts renders SVG with no accessible structure by default, so a screen-reader user gets nothing. Put the numbers somewhere reachable — a Table beside or behind a disclosure, or at minimum a written summary of what the chart shows.

The tooltip appears on hover and on focus of a data point, which means it is pointer-first: it is not a substitute for labelled axes. Keep the axis ticks.

Colour must not be the only difference between series. Two lines in --chart-1 and --chart-3 are the same hue at different lightnesses and will merge for some readers — vary the dash pattern, the marker, or add direct labels.

The chart colours are graphical objects, held to 3:1 against their background rather than the 4.5:1 required of text. Tick labels are text and use muted-foreground, which the contrast audit covers for every theme.

API Reference

Everything inside the container is Recharts' API — see the Recharts reference. ChartLegendContent takes hideIcon and nameKey, and ChartStyle is exported for the rare case of generating the variables yourself.

PropTypeDefaultDescription
configChartConfigMaps each series key to a label, an optional icon, and a colour. Required — it is what the tooltip and legend read, and what the CSS variables are generated from.
idstringa generated idScopes the generated CSS variables. Only worth setting if you need a stable selector for a test or a screenshot.

ChartTooltipContent

PropTypeDefaultDescription
indicator"dot" | "line" | "dashed""dot"The swatch drawn beside each series in the tooltip.
hideLabelbooleanfalseDrop the heading row — for a single-series chart where the label repeats the axis.
hideIndicatorbooleanfalseDrop the colour swatches.
labelKeystringWhich payload field supplies the heading, when it isn't the axis key.
nameKeystringWhich payload field supplies each row's name, when it isn't the series key.
labelFormatter(value, payload) => ReactNodeReformat the heading — a date into a readable month, for instance.
formatter(value, name, item, index, payload) => ReactNodeReformat each row. Use it for units and currency.