Theming
A theme is a single object that sets the default visual style - colors, fonts, strokes, tooltip and legend styles - for every chart below it in the React tree. Instead of repeating stroke, fill and style props on every component in every chart, you set them once.
Themes are experimental. They are exported from the main recharts entry point and they work, but the shape of RechartsTheme can still change in a minor or patch release. Charts without a theme provider keep rendering exactly as they always have.
A theme is the defaults layer. When a single chart needs to differ - one bar with its own shape, one axis with its own tick format, one custom tooltip - that is a prop on that component, and the Customize guide covers it. Explicit props always win over the theme, so the two never fight.
Quick start
Wrap your charts in RechartsThemeProvider and give it a theme:
import { Bar, BarChart, CartesianGrid, Legend, RechartsThemeProvider, Tooltip, XAxis, YAxis, lightTheme } from 'recharts';
export function App() {
return (
<RechartsThemeProvider value={lightTheme}>
<BarChart data={data}>
<CartesianGrid />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="revenue" />
<Bar dataKey="profit" />
</BarChart>
</RechartsThemeProvider>
);
}That is the whole API surface. There is no theme prop on the chart root; a provider around one chart is how you theme one chart.
Built-in themes
Recharts ships three themes:
| Theme | What it does |
|---|---|
lightTheme | A complete light-mode style: dark text, light grid, the familiar Recharts palette. |
darkTheme | The dark-mode counterpart. It does not paint a background behind the chart - that is your page's job. |
emptyTheme | Deliberately blank. It removes all built-in styling so you can style everything yourself, the same idea as a CSS reset. Useful when a chart is fully custom and the legacy defaults get in the way. |
You can try lightTheme and darkTheme right here: the sun / moon button in the navigation bar at the top of this page switches this whole website between the two, and every chart on the page - including the one below - follows along. That button is not a documentation gimmick, it is the real thing: ColorModePicker flips a color mode and the site's layout hands the matching Recharts theme to a single RechartsThemeProvider.
The chart below has no theme provider of its own, so it inherits whatever the page is using. The control lets you swap that inherited theme for the two states the navigation bar cannot express: emptyTheme, and no provider at all. The difference between those two is the interesting one - no provider means every component falls back to its own historical defaults, while emptyTheme means "a theme is in charge, and it says: nothing".
The "no provider" option is worth a closer look: the grid, the axes and the reference line all fall back to the greys they have used since Recharts 2.x, but the bars come out black. That is not a bug - Bar and Scatter have never had a default color, so without a theme you have to pass fill yourself. Line and Area do have a legacy default, but it is a single blue for every series. Giving multi-series charts sensible colors out of the box is one of the reasons themes exist.
Both of those unstyled variants draw black bars, so the example puts them on a white surface. On a dark page they would simply be invisible - which is the honest answer to "what does an unthemed chart look like in dark mode".
How a style is resolved
For every themed property, Recharts asks three questions in order:
- Did you pass the prop explicitly? If yes, your prop wins. Always. A theme never overrides something you wrote in JSX.
- Is there a theme provider, and does its theme contain the relevant section? If yes, the theme value is used.
- Is there no theme provider at all? Then the component uses its own legacy default - the value it has used since Recharts 2.x.
The case worth calling out is the combination of "there is a provider" and "the theme does not define this section". In that case Recharts renders nothing for that property rather than falling back to the legacy default. This is what makes emptyTheme possible, and it means a partial theme is a real design decision: a theme with no grid section gives you an unstyled CartesianGrid, not the default dashed grey one.
| Explicit prop | Provider | Theme section | Result |
|---|---|---|---|
| set | any | any | Your prop. |
| not set | absent | — | Legacy Recharts default. |
| not set | present | present | The theme value. |
| not set | present | missing | No style at all - legacy defaults are intentionally not restored. |
Merging happens field by field, not object by object. If a theme sets stroke and strokeWidth and you only pass stroke, you keep the theme's strokeWidth.
What a theme contains
A theme is a flat set of semantic sections rather than a per-component configuration object. Several components usually share one section, which is what keeps a theme small enough to write by hand.
| Section | Shape | Applies to |
|---|---|---|
graphicalItems (required) | array of { fill, fillOpacity, stroke, strokeWidth, strokeOpacity, strokeDasharray, active } | Area, Bar, Line, Scatter, Radar, RadialBar, Pie, Funnel, Treemap. Legend and Tooltip entries inherit the same colors. |
typography | CSSProperties | Every piece of text Recharts draws: axis ticks, Label, LabelList, Text, and the HTML in Tooltip and Legend. Because it covers both SVG and HTML, color is translated to fill for SVG text automatically. |
axis | { stroke, strokeWidth, strokeOpacity, strokeDasharray } | XAxis, YAxis, PolarAngleAxis, PolarRadiusAxis - their lines and ticks. |
grid | { stroke, strokeWidth, strokeDasharray, fill, fillOpacity } | CartesianGrid and PolarGrid. The grid supports a fill, which is why it takes the 2D shape rather than the line-only one. |
reference | { stroke, strokeWidth, strokeDasharray, fill, fillOpacity } | ReferenceLine, ReferenceArea, ReferenceDot. |
errorBar | { stroke, strokeWidth, ... } | ErrorBar. |
barBackground | { fill, fillOpacity, stroke, ... } | The background rectangles of Bar and RadialBar. |
cursor | { fill, fillOpacity, stroke, ... } | The highlight drawn behind the active tooltip. |
tooltip | { contentStyle, itemStyle, labelStyle } | Tooltip's default content. contentStyle is the wrapper, itemStyle is one data row, labelStyle is the header. |
legend | { wrapperStyle, labelStyle, position, offset } | Legend. Here labelStyle styles the individual entries - the opposite of what the same name means in tooltip. Sorry about that. |
The theme does not set the chart size. Set the chart size with the style prop instead - see the chart size guide.
Colors for multiple series
graphicalItems is an array, and each graphical item in a chart picks one entry from it. The interesting question is which entry.
Recharts does not use render order. Render order is not stable: series get toggled, conditionally rendered, or reordered, and React does not guarantee the traversal order you might expect. Coloring by position means colors jump around when any of that happens.
Instead, most graphical items derive their index from a hash of their dataKey. The same dataKey therefore always gets the same color, in every chart, across renders and reloads.
- Line, Area, Bar, Scatter, Radar and RadialBar hash their
dataKeyinto the array. An item with nodataKeygets no themed color. - Pie, Funnel and Treemap draw many shapes from a single series, so they walk the array by position instead: sector
igets entryi % graphicalItems.length.
Hashing is best-effort, not a guarantee. Two dataKeys can land on the same entry, especially with a short array - "x" and "y" collide in a two-color theme, for instance. A longer palette makes collisions less likely; an explicit fill or stroke prop removes the question entirely.
Each entry can also carry an active block, which styles the highlighted representation of that item: the active dot of Line, Area and Radar, and the active sector of Pie.
Writing your own theme
A RechartsTheme is a plain object: build it inline, import it from a shared module, or fetch it as JSON. Only graphicalItems is required.
import { RechartsThemeProvider, lightTheme, type RechartsTheme } from 'recharts';
/*
* Option 1: start from scratch. Only `graphicalItems` is required.
* Everything you leave out renders unstyled.
*/
const minimalTheme: RechartsTheme = {
graphicalItems: [{ fill: '#4338ca' }, { fill: '#0f766e' }],
};
/*
* Option 2: start from a built-in theme and override what you need.
* These are plain objects, so a spread is all it takes.
*/
const brandTheme: RechartsTheme = {
...lightTheme,
typography: { ...lightTheme.typography, fontFamily: 'Inter, sans-serif' },
grid: { ...lightTheme.grid, stroke: '#e2e8f0', strokeDasharray: '1 4' },
};
export function App() {
return <RechartsThemeProvider value={brandTheme}>...</RechartsThemeProvider>;
}The example below defines a complete theme from scratch and applies it to a line chart:
Nested providers
Providers nest, and the nearest one wins. This is how you give one chart or one section its own look without touching the rest of the page.
One thing to watch: a nested provider replaces the outer theme, it does not merge with it. Recharts reads the nearest theme object and nothing else. If you only want to change the palette, spread the outer theme into the new one - otherwise everything you did not repeat becomes unstyled.
Switching themes at runtime
Dark mode is an ordinary React state update: swap the object you pass to value and leave the chart markup alone. No chart re-declares anything, no component takes a new prop.
import { useState } from 'react';
import { RechartsThemeProvider, darkTheme, lightTheme } from 'recharts';
export function Dashboard() {
const [mode, setMode] = useState<'light' | 'dark'>('light');
return (
<>
<ThemeToggle value={mode} onChange={setMode} />
<RechartsThemeProvider value={mode === 'dark' ? darkTheme : lightTheme}>
<RevenueChart />
<TrafficChart />
</RechartsThemeProvider>
</>
);
}This is not a hypothetical - it is how this website works, and you have been using it all along. The button in the navigation bar cycles light, dark and system; the site layout reads that color mode and renders one RechartsThemeProvider around the entire page with either lightTheme or darkTheme. Every chart in these docs, in every guide and every example, is themed by that single provider. Press it and watch the charts on this page change. See the source code of this page here: on github.
Remember that darkTheme only styles what Recharts draws. The background behind the chart belongs to your page - the site pairs the theme switch with its own CSS color scheme.
Switching with CSS variables instead
Because theme values are handed to the DOM unchanged, any valid CSS value works - including var(--token). That lets a single theme object serve both color schemes and moves the actual switch into CSS, with no React re-render and no flash on server-rendered pages.
/* app.css */
:root {
--chart-text: #18181b;
--chart-grid: #d6d3d1;
--chart-axis: #52525b;
--chart-1: #4338ca;
--chart-2: #0f766e;
--chart-3: #b45309;
}
.dark {
--chart-text: #f5f5f4;
--chart-grid: #3f3f46;
--chart-axis: #d6d3d1;
--chart-1: #a5b4fc;
--chart-2: #5eead4;
--chart-3: #fcd34d;
}import { type RechartsTheme } from 'recharts';
/*
* Theme values are passed to the DOM as-is, so any valid CSS value works,
* including var() references. One theme object then covers both color schemes
* and switching is done in CSS, without a React re-render.
*/
export const cssVariableTheme: RechartsTheme = {
typography: { color: 'var(--chart-text)' },
graphicalItems: [
{ fill: 'var(--chart-1)', stroke: 'var(--chart-1)' },
{ fill: 'var(--chart-2)', stroke: 'var(--chart-2)' },
{ fill: 'var(--chart-3)', stroke: 'var(--chart-3)' },
],
grid: { stroke: 'var(--chart-grid)', fill: 'none' },
axis: { stroke: 'var(--chart-axis)' },
};The same trick works with any token layer: Tailwind and shadcn/ui semantic variables, Chakra's --chakra-* variables, or your own design tokens.
Matching an existing design system
Recharts does not ship adapters for Material UI, Mantine, Chakra, Ant Design or anything else, and it does not need to: a theme is a plain object, so mapping your design system's tokens onto it is a useMemo away.
import { useMemo } from 'react';
import { useTheme } from '@mui/material/styles';
import { RechartsThemeProvider, type RechartsTheme } from 'recharts';
/*
* Recharts ships no adapters for design systems. Because RechartsTheme is a plain
* object, a small mapper in your own code is all you need - and it works the same
* way for Material UI, Mantine, Chakra, Ant Design, or your in-house tokens.
*/
function Charts({ children }) {
const muiTheme = useTheme();
const rechartsTheme = useMemo<RechartsTheme>(
() => ({
typography: {
color: muiTheme.palette.text.primary,
fontFamily: muiTheme.typography.fontFamily,
fontSize: muiTheme.typography.body2.fontSize,
},
graphicalItems: [
{ fill: muiTheme.palette.primary.main, stroke: muiTheme.palette.primary.main },
{ fill: muiTheme.palette.secondary.main, stroke: muiTheme.palette.secondary.main },
{ fill: muiTheme.palette.warning.main, stroke: muiTheme.palette.warning.main },
],
grid: { stroke: muiTheme.palette.divider, fill: 'none' },
axis: { stroke: muiTheme.palette.text.secondary },
tooltip: {
contentStyle: {
backgroundColor: muiTheme.palette.background.paper,
border: `1px solid ${muiTheme.palette.divider}`,
borderRadius: muiTheme.shape.borderRadius,
},
},
}),
[muiTheme],
);
return <RechartsThemeProvider value={rechartsTheme}>{children}</RechartsThemeProvider>;
}Reading the theme yourself
Custom shapes, custom tooltip content and custom legends are your components, so Recharts does not style them - see the Customize guide for how those render props work. If you want them to follow the theme, read it with useRechartsTheme. It returns the nearest theme as-is, with no defaults applied, and undefined when there is no provider.
import { useRechartsTheme } from 'recharts';
/*
* Read the active theme from your own components, for example to paint
* the page background or a custom Tooltip content that matches the chart.
*/
function CustomTooltipContent({ payload }) {
const theme = useRechartsTheme();
return <div style={theme?.tooltip?.contentStyle}>{payload[0]?.value}</div>;
}Current limitations
- The API is marked experimental.
RechartsThemecan change shape in a minor or patch release. - Not every component is themed yet. Sankey and SunburstChart still use their own defaults regardless of the theme.
- Themes are not deep-merged. Nested providers replace, and there is no
createThemehelper to merge for you. - Series colors are assigned by a hash, which is stable but not collision-free. Use explicit props when a specific color matters.