recharts
Recharts renders charts as React components that draw SVG. Instead of configuring a chart with one big options object, you compose it: a LineChart wrapping an XAxis, a YAxis, a CartesianGrid, a Tooltip, and one Line per series, each an independent component with its own props. The maths comes from d3 modules (bundled through victory-vendor) but you never touch a d3 selection. Because everything is SVG in the React tree, you style it with CSS, hang event handlers off individual elements, and swap in your own React component for the tooltip, legend, axis tick, or bar shape. Version 3 rewrote the internals onto a Redux Toolkit store, which is why the dependency list is longer than you might expect for a chart library.
The default React charting choice for a reason: composition makes customisation genuinely easy and the defaults look fine. Pay attention to the 144 KB and to the SVG-per-datum rendering model, because those are the two things that turn a good decision into a bad one later.
Use it if
- You are in React and want charts described the same way as the rest of your UI, with composition instead of a config object
- You need to replace parts of the chart with your own React components: custom tooltips, custom legend rows, custom axis ticks, and custom bar shapes are all just a content or shape prop
- You want reasonable dashboard defaults without design work, since a LineChart with an axis, a grid, and a tooltip is about eight lines and already looks presentable
- You need charts on a page that syncs interactions: syncId ties tooltips and Brush ranges across several charts with one prop
- Bundle size is a real constraint. About 144 KB gzipped and eleven runtime dependencies (including @reduxjs/toolkit, immer, react-redux, and the d3 bundle in victory-vendor) is a lot to ship for one dashboard panel
- You are plotting more than a few thousand points. Every datum becomes SVG in the DOM, so scatter plots and dense time series get slow in a way no prop fixes; a canvas or WebGL renderer is the right tool there
- You need built-in zoom and pan, cross-filtering, or annotation editing. Brush gives you a range selector and ReferenceLine gives you static markers, and beyond that you are building it
- Server rendering must produce a visible chart. ResponsiveContainer measures its parent in the browser before it draws anything, so SSR output is an empty box and the chart appears after hydration
- You are still on Recharts 2 and expecting a quick upgrade. Version 3 moved internal state into a Redux store and dropped defaultProps on function components, so custom shapes and components that relied on injected props need rework
- You need long-tail support. There are 398 open issues on top of open PRs, and the theming API added recently is still marked experimental and currently only styles the grid
Setup reality
npm install recharts react-is, and the react-is part is not optional: the README says it must match your installed react version, and a mismatch shows up as components silently not rendering rather than as an install error. Peer ranges cover React 16.8 through 19 and Node 18 or newer. The first thing that goes wrong for almost everyone is ResponsiveContainer collapsing to zero height, because it fills its parent and the parent has no height; give the wrapper an explicit height or aspect ratio. The second is chart flicker in SSR frameworks, which the initialDimension prop softens but does not remove. The published package has a CommonJS main and an ES module entry but no exports map, so some bundler setups resolve the CJS build and you pay for the whole library. Documentation is split between recharts.github.io, a Storybook, and a GitHub wiki, and the README warns that the site reflects the release branch while development happens on main.
Patterns
A line chart that fills its containerresponsive-line-chart
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
<div style={{ width: '100%', height: 320 }}>
<ResponsiveContainer>
<LineChart data={data} margin={{ top: 8, right: 16, bottom: 8, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="revenue" stroke="#2563eb" dot={false} />
</LineChart>
</ResponsiveContainer>
</div>The wrapping div needs a concrete height. ResponsiveContainer defaults to 100% of its parent, and a parent with auto height resolves to zero, which is the number one reason a Recharts chart renders as nothing.
Replace the tooltip with your own componentcustom-tooltip
import { Tooltip } from 'recharts';
function CustomTooltip({ active, payload, label }) {
if (!active || !payload?.length) return null;
return (
<div className="rounded border bg-white p-2 text-sm shadow">
<div className="font-medium">{label}</div>
{payload.map(entry => (
<div key={entry.dataKey} style={{ color: entry.color }}>
{entry.name}: {entry.value}
</div>
))}
</div>
);
}
<Tooltip content={<CustomTooltip />} />Return HTML, not SVG: the tooltip is rendered in a positioned div outside the chart's svg element. Always bail out when active is false, because the component is mounted before there is any payload.
Format ticks and tooltip numbersformat-axis-and-tooltip-values
const currency = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 });
<XAxis
dataKey="date"
tickFormatter={value => new Date(value).toLocaleDateString('en-US', { month: 'short' })}
minTickGap={24}
/>
<YAxis tickFormatter={currency.format} width={72} />
<Tooltip formatter={value => currency.format(value)} />Give YAxis an explicit width when you format to long labels, otherwise the axis clips them; minTickGap on XAxis is the fix for overlapping date labels, not rotating them.
Stack several seriesstacked-area-chart
import { AreaChart, Area, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer } from 'recharts';
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data}>
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Legend />
<Area type="monotone" dataKey="free" stackId="plan" stroke="#94a3b8" fill="#cbd5e1" />
<Area type="monotone" dataKey="pro" stackId="plan" stroke="#2563eb" fill="#93c5fd" />
</AreaChart>
</ResponsiveContainer>Series stack when they share a stackId string; series with different stackIds sit side by side. Add stackOffset="expand" on the chart to turn the same data into a 100 percent stacked view.
Bars and a line on two Y axescomposed-chart-dual-axis
import { ComposedChart, Bar, Line, XAxis, YAxis, Tooltip, Legend } from 'recharts';
<ComposedChart width={640} height={320} data={data}>
<XAxis dataKey="week" />
<YAxis yAxisId="left" />
<YAxis yAxisId="right" orientation="right" />
<Tooltip />
<Legend />
<Bar yAxisId="left" dataKey="signups" fill="#93c5fd" />
<Line yAxisId="right" type="monotone" dataKey="conversionRate" stroke="#dc2626" />
</ComposedChart>Every series must carry the yAxisId of the axis it belongs to. Omit it and the series silently binds to the default axis id of 0, which is usually the wrong scale rather than an error.
Colour each slice individuallypie-chart-with-per-slice-colors
import { PieChart, Pie, Cell, Tooltip, Legend } from 'recharts';
const COLORS = ['#2563eb', '#16a34a', '#f59e0b', '#dc2626'];
<PieChart width={320} height={320}>
<Pie data={data} dataKey="value" nameKey="label" innerRadius={60} outerRadius={110} paddingAngle={2}>
{data.map((entry, i) => (
<Cell key={entry.label} fill={COLORS[i % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>Cell children are matched to slices by index, so a sorted or filtered data array reshuffles the colours; key off a stable field and derive the colour from the datum if the order can change.
Draw a target or threshold linereference-line
import { ReferenceLine, ReferenceArea } from 'recharts';
<ReferenceLine y={target} stroke="#dc2626" strokeDasharray="4 4" label={{ value: 'target', position: 'right' }} />
<ReferenceArea x1="2026-03-01" x2="2026-03-14" fill="#f1f5f9" ifOverflow="extendDomain" />By default a reference outside the current axis domain is hidden; pass ifOverflow="extendDomain" to grow the axis to include it, or "visible" to clamp it into view.
Let users scrub a window of a long seriesbrush-range-selector
import { LineChart, Line, XAxis, YAxis, Brush } from 'recharts';
<LineChart width={720} height={360} data={data}>
<XAxis dataKey="date" />
<YAxis />
<Line type="monotone" dataKey="value" dot={false} />
<Brush
dataKey="date"
height={28}
startIndex={Math.max(0, data.length - 90)}
onChange={({ startIndex, endIndex }) => setRange([startIndex, endIndex])}
/>
</LineChart>Brush works on array indices, not data values, so translate through your data array before using the range elsewhere. It filters what is drawn but every point stays in the DOM, so it does not rescue a chart that is slow from point count.
Link tooltips across stacked chartssync-multiple-charts
<LineChart data={cpu} syncId="host-metrics" width={640} height={160}>{/* ... */}</LineChart>
<LineChart data={memory} syncId="host-metrics" width={640} height={160}>{/* ... */}</LineChart>Charts sharing a syncId share tooltip position and Brush range. Add syncMethod="value" when the charts have different row counts, since the default matches on index and will line up the wrong points.
Keep the chart from re-animating on every renderstop-animation-restarts
const chartData = useMemo(() => rows.map(toPoint), [rows]);
<Line
type="monotone"
dataKey="value"
isAnimationActive={false}
dot={false}
/>A new data array identity on each parent render replays the entrance animation and makes the chart look like it is flickering. Memoize the array, and turn animation off entirely for charts that update on an interval.
Build a custom child that knows the chart stateread-chart-state-with-hooks
import { Customized, useActiveTooltipDataPoints, usePlotArea } from 'recharts';
function ActiveReadout() {
const points = useActiveTooltipDataPoints();
const plot = usePlotArea();
if (!points?.length || !plot) return null;
return (
<text x={plot.x + 8} y={plot.y + 16} fontSize={12}>
{points[0].value}
</text>
);
}
<LineChart data={data} width={640} height={320}>
<Line dataKey="value" />
<Customized component={<ActiveReadout />} />
</LineChart>These hooks are the version 3 replacement for reaching into internal context and only work inside a chart's subtree; in version 2 the same job meant reading props that the chart injected into cloned children.
Get real types on dataKey instead of anytype-safe-chart-components
import { createHorizontalChart } from 'recharts';
type Point = { date: string; revenue: number };
const { LineChart, Line, XAxis, YAxis, Tooltip } = createHorizontalChart<Point, string, number>();
<LineChart data={points} width={640} height={320}>
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Line dataKey="revenue" />
</LineChart>Recharts components fall back to any for dataKey by default, so a typo in a series name fails silently at runtime. The factory helpers narrow dataKey to your data's own keys and catch it at compile time.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @visx/xychart | npm | You want d3 power with React rendering and are willing to assemble the chart from primitives yourself |
| chart.js | npm | You want canvas rendering for large datasets and do not need the chart to be part of the React tree |
| echarts | npm | You need heavy interactivity, zoom and pan, maps, or chart types Recharts does not have |
| @nivo/core | npm | You want React charts with more layout types and built-in animation presets, and can accept a similar bundle cost |