Skip to main content

Recharts vs Chart.js vs Nivo vs Visx: React Charting in 2026

·PkgPulse Team

TL;DR

For most React dashboards: Recharts is the pragmatic choice — declarative JSX API, solid TypeScript types, and common chart types covered without much boilerplate. Chart.js (via react-chartjs-2) is the battle-tested option for vanilla JS or when you need maximum chart variety. Nivo gives you beautiful, accessible defaults at the cost of bundle size. Visx (Airbnb, powered by D3) is for teams that need maximum control and are willing to compose primitives from scratch.

Key Takeaways

  • Recharts: ~1.8M weekly downloads — declarative, React-first, reasonable bundle size
  • Chart.js: ~4.1M weekly downloads — most popular JS charting library overall, non-React-native
  • Nivo: ~450K weekly downloads — beautiful defaults, SVG + Canvas, great accessibility
  • Visx: ~300K weekly downloads — D3-powered primitives, maximum flexibility, steep learning curve
  • Recharts wins for straightforward dashboards with standard chart types
  • Visx wins when you need highly custom, interactive data visualizations
  • Chart.js's high downloads are largely non-React usage

PackageWeekly DownloadsRenderingBundle Size
chart.js~4.1MCanvas~65KB
react-chartjs-2~1.6MCanvas (via Chart.js)~1KB (wrapper)
recharts~1.8MSVG~370KB
@nivo/core~450KSVG/Canvas~40KB core
@visx/shape~300KSVG (D3)Modular

Rendering: SVG vs Canvas

SVG (Recharts, Nivo, Visx):

  • CSS-styleable — inspect elements, apply classes
  • Accessible — screen reader support, ARIA attributes
  • Scales infinitely without pixelation
  • Slower for large datasets (thousands of data points)

Canvas (Chart.js):

  • Faster rendering for large datasets
  • Not accessible by default (single <canvas> element)
  • Can't style individual elements with CSS
  • Renders sharper at high DPI

Recharts

Recharts was specifically designed for React — charts are JSX components, not configuration objects:

import {
  LineChart,
  Line,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  Legend,
  ResponsiveContainer,
} from "recharts"

const downloadData = [
  { month: "Oct", recharts: 1400, nivo: 280, visx: 180 },
  { month: "Nov", recharts: 1500, nivo: 310, visx: 195 },
  { month: "Dec", recharts: 1650, nivo: 340, visx: 215 },
  { month: "Jan", recharts: 1700, nivo: 390, visx: 240 },
  { month: "Feb", recharts: 1800, nivo: 450, visx: 300 },
]

function DownloadChart() {
  return (
    <ResponsiveContainer width="100%" height={300}>
      <LineChart data={downloadData} margin={{ top: 5, right: 30, bottom: 5, left: 0 }}>
        <CartesianGrid strokeDasharray="3 3" />
        <XAxis dataKey="month" />
        <YAxis />
        <Tooltip formatter={(value) => `${value.toLocaleString()}/wk`} />
        <Legend />
        <Line type="monotone" dataKey="recharts" stroke="#8884d8" strokeWidth={2} dot={false} />
        <Line type="monotone" dataKey="nivo" stroke="#82ca9d" strokeWidth={2} dot={false} />
        <Line type="monotone" dataKey="visx" stroke="#ffc658" strokeWidth={2} dot={false} />
      </LineChart>
    </ResponsiveContainer>
  )
}

Custom Recharts tooltip:

const CustomTooltip = ({ active, payload, label }: TooltipProps<number, string>) => {
  if (!active || !payload?.length) return null

  return (
    <div className="bg-white border rounded shadow p-3">
      <p className="font-semibold">{label}</p>
      {payload.map((entry) => (
        <p key={entry.dataKey} style={{ color: entry.color }}>
          {entry.name}: {entry.value?.toLocaleString()}/wk
        </p>
      ))}
    </div>
  )
}

Recharts chart types: Line, Bar, Area, Pie, Radar, Scatter, Treemap, Funnel, RadialBar

Recharts limitations:

  • Bundle is large (~370KB) — includes everything
  • Complex composited charts can get verbose
  • Animation customization is limited

Chart.js via react-chartjs-2

Chart.js is the most widely used JavaScript charting library, with react-chartjs-2 as the thin React wrapper:

import {
  Chart as ChartJS,
  CategoryScale,
  LinearScale,
  PointElement,
  LineElement,
  Title,
  Tooltip,
  Legend,
} from "chart.js"
import { Line } from "react-chartjs-2"

// Must register components (tree-shaking):
ChartJS.register(
  CategoryScale, LinearScale, PointElement, LineElement,
  Title, Tooltip, Legend
)

const data = {
  labels: ["Oct", "Nov", "Dec", "Jan", "Feb"],
  datasets: [
    {
      label: "Recharts",
      data: [1400, 1500, 1650, 1700, 1800],
      borderColor: "rgb(136, 132, 216)",
      backgroundColor: "rgba(136, 132, 216, 0.1)",
      tension: 0.4,
    },
  ],
}

const options = {
  responsive: true,
  plugins: {
    legend: { position: "top" as const },
    title: { display: true, text: "Weekly Downloads" },
  },
  scales: {
    y: { beginAtZero: false },
  },
}

function ChartJSExample() {
  return <Line data={data} options={options} />
}

Chart.js strengths:

  • 30+ chart types including specialized charts (bubble, polar area, financial)
  • Canvas rendering handles 10,000+ data points
  • Extensive plugin ecosystem (zoom, annotation, financial)
  • Configuration-based — easy to understand and modify

Chart.js for React limitations:

  • Configuration object, not JSX — less React-idiomatic
  • TypeScript types require separate @types/chart.js (v3+ has built-in types)
  • react-chartjs-2 adds a React-specific API layer that can feel disconnected
  • Animation on data updates requires careful ref management

Nivo

Nivo provides complete, production-ready chart components with beautiful defaults:

import { ResponsiveLine } from "@nivo/line"

const nivoData = [
  {
    id: "recharts",
    color: "hsl(248, 70%, 50%)",
    data: [
      { x: "Oct", y: 1400 },
      { x: "Nov", y: 1500 },
      { x: "Dec", y: 1650 },
      { x: "Jan", y: 1700 },
      { x: "Feb", y: 1800 },
    ],
  },
]

function NivoChart() {
  return (
    <div style={{ height: 300 }}>
      <ResponsiveLine
        data={nivoData}
        margin={{ top: 50, right: 110, bottom: 50, left: 60 }}
        xScale={{ type: "point" }}
        yScale={{ type: "linear", min: "auto", max: "auto" }}
        curve="cardinal"
        axisBottom={{ legend: "Month", legendOffset: 36 }}
        axisLeft={{ legend: "Downloads", legendOffset: -40 }}
        pointSize={8}
        useMesh={true}  // Performance optimization for hover
        legends={[{
          anchor: "bottom-right",
          direction: "column",
          itemWidth: 100,
          itemHeight: 20,
        }]}
      />
    </div>
  )
}

Nivo's standout features:

// Built-in canvas mode for large datasets:
import { ResponsiveLineCanvas } from "@nivo/line"
// Same API as SVG version, renders via Canvas for performance

// Motion/animation — all charts have spring animations by default
// Accessibility — ARIA labels, keyboard navigation built-in

// Available chart types (all with ResponsiveXxx wrappers):
// Bar, Line, Pie, Scatter, HeatMap, Treemap, Sankey, Chord,
// Calendar, Waffle, Bump, Stream, SwarmPlot, CirclePacking, and more

Nivo limitations:

  • Per-chart packages — @nivo/line, @nivo/bar, etc. add up in bundle size
  • Less low-level control than Visx
  • Config-heavy — more props than Recharts

Visx

Visx (by Airbnb) gives you D3's power through React components — low-level primitives to compose any chart:

import { LinePath, AreaClosed, Bar } from "@visx/shape"
import { Group } from "@visx/group"
import { scaleLinear, scaleBand, scaleTime } from "@visx/scale"
import { AxisBottom, AxisLeft } from "@visx/axis"
import { GridRows, GridColumns } from "@visx/grid"
import { Tooltip, useTooltip, TooltipWithBounds } from "@visx/tooltip"
import { localPoint } from "@visx/event"
import { curveMonotoneX } from "@visx/curve"

interface DataPoint {
  date: Date
  value: number
}

interface LineChartProps {
  data: DataPoint[]
  width: number
  height: number
  margin?: { top: number; right: number; bottom: number; left: number }
}

function VisxLineChart({ data, width, height, margin = { top: 20, right: 20, bottom: 40, left: 50 } }: LineChartProps) {
  const innerWidth = width - margin.left - margin.right
  const innerHeight = height - margin.top - margin.bottom

  // Define scales manually — full D3 control:
  const xScale = scaleTime({
    range: [0, innerWidth],
    domain: [Math.min(...data.map(d => d.date.getTime())), Math.max(...data.map(d => d.date.getTime()))],
  })

  const yScale = scaleLinear({
    range: [innerHeight, 0],
    domain: [0, Math.max(...data.map(d => d.value))],
    nice: true,
  })

  const { tooltipData, tooltipLeft, tooltipTop, showTooltip, hideTooltip } = useTooltip<DataPoint>()

  return (
    <svg width={width} height={height}>
      <Group left={margin.left} top={margin.top}>
        <GridRows scale={yScale} width={innerWidth} stroke="#e0e0e0" />
        <GridColumns scale={xScale} height={innerHeight} stroke="#e0e0e0" />

        <LinePath
          data={data}
          x={(d) => xScale(d.date) ?? 0}
          y={(d) => yScale(d.value) ?? 0}
          stroke="#8884d8"
          strokeWidth={2}
          curve={curveMonotoneX}
        />

        {/* Invisible hover bars for tooltip capture */}
        {data.map((d, i) => (
          <Bar
            key={i}
            x={xScale(d.date) - 5}
            y={0}
            width={10}
            height={innerHeight}
            fill="transparent"
            onMouseMove={(event) => {
              const coords = localPoint(event)
              showTooltip({ tooltipData: d, tooltipLeft: coords?.x, tooltipTop: coords?.y })
            }}
            onMouseLeave={hideTooltip}
          />
        ))}

        <AxisBottom top={innerHeight} scale={xScale} tickFormat={(d) => d.toLocaleDateString()} />
        <AxisLeft scale={yScale} />
      </Group>

      {tooltipData && (
        <TooltipWithBounds top={tooltipTop} left={tooltipLeft}>
          {tooltipData.value.toLocaleString()}/wk
        </TooltipWithBounds>
      )}
    </svg>
  )
}

Visx requires significantly more code but every pixel is intentional.


Feature Comparison

FeatureRechartsChart.jsNivoVisx
React-native API✅ JSX⚠️ Config✅ Props✅ Components
TypeScript
RenderingSVGCanvasSVG + CanvasSVG
Large datasets (10K+)⚠️ Slow✅ Canvas✅ Canvas mode✅ With optimization
Customization level⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Chart types10+30+30+Unlimited (primitives)
Default aesthetics⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐DIY
Accessibility⚠️ Partial⚠️ Limited✅ Excellent✅ (manual)
Animation✅ Spring-based✅ D3 transitions
Bundle size~370KB~65KBModularModular
Learning curveLowLowMediumHigh

When to Use Each

Choose Recharts if:

  • Building standard dashboards (line, bar, area, pie)
  • You want JSX-first React integration without config objects
  • Developer velocity matters more than pixel-perfect customization

Choose Chart.js (react-chartjs-2) if:

  • You have an existing Chart.js codebase
  • You need canvas performance for thousands of data points
  • You need specialized chart types (bubble, financial, polar)

Choose Nivo if:

  • You want beautiful, accessible charts out of the box
  • You need specialized chart types (sankey, heatmap, waffle, chord)
  • Accessibility and screen reader support are priorities

Choose Visx if:

  • You need fully custom data visualizations that no library covers
  • Your team knows D3 and wants React integration without abstraction
  • Building complex interactive charts (brush selection, custom interactions)

Methodology

Download data from npm registry (weekly average, February 2026). Bundle sizes from bundlephobia. Chart type counts from official documentation. Learning curve assessment based on community feedback and API complexity.

Compare charting library packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.