TL;DR: Nivo wraps D3 in declarative React components. You pass data and props, it draws the chart. Every family is a separate package, SVG variants render server-side, and the docs site builds the code for you as you click through options. Docs: nivo.rocks.
What is Nivo?
Nivo is a chart library built on top of D3's math, with React doing the rendering. That split matters. D3 gives it scales, shapes and layouts that have been correct for a decade; React means you describe a chart as <ResponsiveBar data={data} keys={keys} /> rather than imperatively selecting and appending nodes.
The catalogue is wide: bar, line, pie, scatter, heatmap, treemap, sunburst, sankey, calendar, chord, network, geo, radar, bullet, funnel. Most families ship in two flavours, SVG and Canvas, sometimes with an HTML variant too.
When to use it
Reach for Nivo when you want charts that look considered without designing them, and when you need more than the six chart types a lighter library gives you. It's a good fit for internal dashboards and for editorial charts where a treemap or a sankey is the right answer and you don't want to hand-roll one.
Look elsewhere if your whole need is three bar charts. A single-purpose library, or plain SVG, will cost you less. Nivo's floor is higher than a minimal library's because @nivo/core comes along with whichever family you install.
Key Features
- Declarative props for every chart family, with the same shape across families
- SVG and Canvas renderers for the same chart, swappable when point counts grow
- Server-side rendering for SVG charts, which matters in Next.js and Astro
- Motion via react-spring, on by default, disabled with
animate={false} - A theme object that covers axes, grid, labels, tooltips and legends in one place
- Responsive wrappers (
ResponsiveBar,ResponsiveLine) that fill their container
Installation
Install the core plus the families you actually render:
npm install @nivo/core @nivo/bar
React 18 or newer is required. Each additional family is its own install:
npm install @nivo/line @nivo/pie
A Bar Chart
import { ResponsiveBar } from '@nivo/bar';
type Row = { quarter: string; shipped: number; planned: number };
const data: Row[] = [
{ quarter: 'Q1', shipped: 42, planned: 50 },
{ quarter: 'Q2', shipped: 61, planned: 55 },
{ quarter: 'Q3', shipped: 48, planned: 58 },
];
export function ReleaseChart() {
return (
<div style={{ height: 320 }}>
<ResponsiveBar
data={data}
keys={['shipped', 'planned']}
indexBy="quarter"
groupMode="grouped"
margin={{ top: 16, right: 16, bottom: 40, left: 48 }}
axisBottom={{ legend: 'Quarter', legendOffset: 32 }}
axisLeft={{ legend: 'Features', legendOffset: -40 }}
colors={{ scheme: 'set2' }}
/>
</div>
);
}
The wrapper needs an explicit height. ResponsiveBar measures its parent, and a parent that sizes to its content collapses to zero.
Theming It Once
Define the theme next to your design tokens and pass it to every chart:
const chartTheme = {
background: 'transparent',
text: { fontSize: 12, fill: 'var(--color-text)' },
axis: {
ticks: { line: { stroke: 'var(--color-border)' } },
legend: { text: { fontSize: 13 } },
},
grid: { line: { stroke: 'var(--color-border)', strokeDasharray: '3 3' } },
tooltip: { container: { background: 'var(--color-surface)' } },
};
Because it merges with the default, you only write the parts you're changing.
TypeScript Tips
Chart props are generic over your datum type, so type the data and let inference do the rest. Where a prop takes a callback, the datum arrives typed:
<ResponsiveBar<Row>
data={data}
keys={['shipped']}
indexBy="quarter"
colors={({ data }) => (data.shipped < data.planned ? '#e76f51' : '#2a9d8f')}
/>
Custom layers and tooltips are the two places worth annotating explicitly, since their props are wide unions.
Common Gotchas
The most common one is a chart that renders nothing: the container has no height, so the responsive wrapper measures zero. Give the parent a fixed height or a grid row that resolves.
The second is bundle size creeping up. Each family pulls its own D3 modules, so five families is meaningfully more than one. Check what you're importing before adding a sixth.
The third is animation on large datasets. Motion is on by default and it costs real frames at a few thousand elements. Set animate={false} for dense charts, or move to the Canvas variant.