Skip to content

Frontend

Recharts vs Victory vs D3 — Which Chart Library Ships in Your SaaS Dashboard

Your dashboard needs real data visualization, but D3 is overkill, Victory feels bloated, and rolling your own SVG is a road to hell. Recharts: declarative React components, CSS variable theming, ResponsiveContainer that handles resize, composable tooltips and legends. This is how production SaaS charts ship in 2026.

📊 🎨

Three weeks ago you inherited a dashboard. The previous team hand-rolled SVG with D3 v4 — thirty invisible dependencies, a 120KB bundle, and a tooltip that breaks when you resize the window. Every time a designer asks for a colour change, you're digging into Scalable Vector Graphics instead of updating a token. When mobile users shrink the viewport, the chart gets clipped. New requirement: add a legend. That's another 400 lines of path math.

This is why most SaaS teams reach for a library. But which one? Material-UI bundled 90KB just for charts (and you're using V5, not V6). Victory is cleaner than D3 but still verbose — you're writing 15 props for a line chart that should take 3. Recharts landed in 2015 and nobody talks about it anymore, but Velocity X's dashboards ship on it because it's the only one that feels designed for React component thinking: declarative, composable, and your colors live in CSS variables.

Three Libraries, Three Philosophies

D3: Low-level rendering engine. You own every pixel. Bundle: 35KB (d3-core). Data binding, scales, axes — all yours to orchestrate. Ceiling is limitless if you're willing to spend weeks. Floor is "I need a line chart and a tooltip," which should take 5 minutes but takes 5 days. Best if: your chart is bespoke (custom interaction, weird data shape, academic visualization). Worst if: you just need insurance dashboards to look polished without engineering bravery.

Victory: Component-driven wrapper around d3-scale and d3-shape. Bundle: 45KB. Better than raw D3 because you import a LineChart component and hand it data. But you're still prop-drilling every customization (axis labels, grid, tooltips, legends). Want a custom tooltip? Pass a VictoryTooltip component with twenty props. Three projects in, you've built a wrapper component that wraps the wrapper. Velocity X tested Victory on the Rebuild Relief staff tool, felt heavier than the benefit, and jumped.

Recharts: React-first composable chart library built on Visx (Visx is Victory's smarter cousin). Bundle: 60KB but you're likely already importing React, PropTypes, classnames. Declarative: you nest LineChart, Line, CartesianGrid, Tooltip, Legend — reads like JSX. Responsive by default: ResponsiveContainer measures the parent and reflows when the viewport changes. Colors from CSS tokens: <Line dataKey="revenue" stroke="var(--primary)" />. Ecosystem: built-in composition for multi-series, stacked charts, area fills, custom tooltips with Recharts' Shape utilities. This is what a React person would design if they ignored D3 maturity and just built "how should this feel?" Velocity X's dashboards (revenue forecasts, job location heatmaps, team capacity) all use Recharts because the code is boring in the best way.

The Comparison Table

Dimension D3 Victory Recharts
Bundle 35KB gzip 45KB gzip 60KB gzip
Learning Curve Steep (math required) Moderate (many props) Shallow (composable JSX)
Responsive Write your own Needs library ResponsiveContainer built-in
Customization Infinite (raw SVG) High (props + style) High (props + composition)
CSS Theming Manual (calculateColor) Manual (theme prop) Native (CSS variables)
Best For Academic / bespoke Custom styling SaaS dashboards

Minimal Recharts Line Chart

Here's what a revenue forecast chart looks like in Recharts. No state management, no calculations, no resize observer:

import { LineChart, Line, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';

const data = [
  { month: 'Jan', revenue: 4000, forecast: 2400 },
  { month: 'Feb', revenue: 3000, forecast: 2200 },
  { month: 'Mar', revenue: 2000, forecast: 2290 },
  { month: 'Apr', revenue: 2780, forecast: 2000 },
  { month: 'May', revenue: 1890, forecast: 2181 },
  { month: 'Jun', revenue: 2390, forecast: 2500 },
];

export function RevenueChart() {
  return (
    <ResponsiveContainer width="100%" height={400}>
      <LineChart data={data}>
        <CartesianGrid strokeDasharray="3 3" />
        <Tooltip />
        <Legend />
        <Line type="monotone" dataKey="revenue" stroke="var(--primary)" strokeWidth={2} />
        <Line type="monotone" dataKey="forecast" stroke="var(--accent)" strokeWidth={2} strokeDasharray="5 5" />
      </LineChart>
    </ResponsiveContainer>
  );
}

That's it. ResponsiveContainer watches the parent element and reflows on resize. Tooltip appears on hover with zero extra code. Legend is one line. Lines pull colors from CSS variables — swap the token, every chart updates. This is Recharts: you describe *what* you want, not *how* to render it.

Four Production Patterns

1. Custom Tooltip — Contextual Data

Default Recharts tooltip is a gray box. Real dashboards need context: show the trend, highlight the outlier, link to details. Write a custom component:

function CustomTooltip({ active, payload }) {
  if (!active || !payload?.length) return null;

  const { revenue, forecast } = payload[0].payload;
  const variance = ((revenue - forecast) / forecast * 100).toFixed(1);

  return (
    <div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded p-3 shadow-lg">
      <p className="text-sm font-semibold">{payload[0].payload.month}</p>
      <p className="text-xs text-slate-600 dark:text-slate-400">Actual: ${revenue.toLocaleString()}</p>
      <p className="text-xs text-slate-600 dark:text-slate-400">Forecast: ${forecast.toLocaleString()}</p>
      <p className={`text-xs font-semibold ${variance > 0 ? 'text-green-600' : 'text-red-600'}`}>
        Variance: {variance}%
      </p>
    </div>
  );
}

// Then in your chart:
<Tooltip content={<CustomTooltip />} />

Plug in the component, reference the payload properties, and your tooltip now speaks business language. No magic, no escaping the render loop.

2. Brand Colors via CSS Variables

Velocity projects define primary, secondary, accent in globals.css. Recharts reads them as strings:

// globals.css
:root {
  --primary: 265 90% 50%;        /* Purple for your brand */
  --secondary: 0 0% 8.8%;        /* Near-black */
  --accent: 48 96% 53%;          /* Gold */
  --success: 120 100% 40%;       /* Green */
  --warning: 45 93% 51%;         /* Amber */
  --destructive: 0 84% 60%;      /* Red */
}

// In your chart:
<Line dataKey="revenue" stroke={`hsl(var(--primary))`} />
<Line dataKey="forecast" stroke={`hsl(var(--accent))`} />
<Area dataKey="capacity" fill={`hsl(var(--success) / 0.1)`} />

When the design system shifts (rebranding HailHero or onboarding Staff Operations Dashboard), the tokens change once. Every chart updates automatically. No component prop drilling, no theme context, no Zustand selector overhead.

3. Responsive Stacked Area + Grid

Multi-series stacked area is common in SaaS (revenue by product, capacity by team). Recharts composes it like building blocks:

<ResponsiveContainer width="100%" height={400}>
  <AreaChart data={data}>
    <defs>
      <linearGradient id="colorRevenue" x1="0" y1="0" x2="0" y2="1">
        <stop offset="5%" stopColor={`hsl(var(--primary))`} stopOpacity={0.8}/>
        <stop offset="95%" stopColor={`hsl(var(--primary))`} stopOpacity={0}/>
      </linearGradient>
    </defs>
    <CartesianGrid strokeDasharray="3 3" vertical={false} />
    <Tooltip />
    <Area type="monotone" dataKey="productA" stackId="1" fill="hsl(var(--primary))" />
    <Area type="monotone" dataKey="productB" stackId="1" fill="hsl(var(--accent))" />
    <Area type="monotone" dataKey="productC" stackId="1" fill="hsl(var(--secondary))" />
  </AreaChart>
</ResponsiveContainer>

Gradients, stacking, grid off on the y-axis — all one-liners. Mobile? ResponsiveContainer shrinks the chart, axes rotate, spacing adjusts. No JavaScript media queries, no state machines.

4. Animations On Load

Dashboards feel alive when charts animate in. Recharts has built-in easing:

<LineChart data={data} margin={{ top: 5, right: 30, left: 0, bottom: 5 }}>
  <Line
    type="monotone"
    dataKey="revenue"
    stroke="var(--primary)"
    dot={false}
    isAnimationActive={true}
    animationDuration={800}
    animationEasing="ease-in-out"
  />
</LineChart>

Or wrap the chart in a Framer Motion <motion.div> and tie animations to parent scroll or viewport entry. Recharts doesn't fight with other animation libraries — it just renders, and you layer choreography on top.

Six FAQs

D3 is legendary. Why not just learn D3?

D3 is a 35KB engine. Learning D3 means learning scales, domains, ranges, selections, data binding, and SVG paths. For a dashboard, you're spending engineering effort on "how to draw" instead of "what insights to show." D3 wins if your chart is an anomaly (geographic map with custom projection, force-directed graph, academic network). For CRUD dashboards (line, bar, area, pie), Recharts is 100× faster to ship. Skip D3 unless the chart is genuinely novel.

What about Visx (from Airbnb)?

Visx is D3 primitives in React (scales, axes, shapes). It's lighter than D3 but more work than Recharts. Use Visx if you're building a reusable charting library for your org; use Recharts if you're building dashboards for your customers.

How do I handle real-time data (WebSocket updates)?

Recharts doesn't care where the data comes from. Update state, pass it to the chart, it reflows. Use Zustand for chart state, subscribe to WebSocket events, dispatch to Zustand, component re-renders, chart updates. No special Recharts APIs — just React state management.

Can Recharts handle 10K data points without choking?

Recharts renders SVG, so 10K points = 10K DOM nodes. SVG rendering tanks at ~3–5K points depending on browser and interactivity. If you need dense time-series (stock charts, sensor data), use canvas (Recharts doesn't support it natively). Alternatives: Visx (you own the rendering), Apache ECharts (canvas-based, heavy), or deck.gl (WebGL, overkill for most dashboards). For typical SaaS (daily/weekly aggregates), Recharts handles 500–2K points easily.

Does Recharts work with TypeScript?

Yes. Recharts has full TypeScript support (via DefinitelyTyped). Your data shape is loose, but that's on you to validate with Zod before rendering. Component props are fully typed.

How do I export charts as PNG/PDF?

Use html2canvas (PNG) or jsPDF + html2canvas (PDF). Recharts renders SVG, so the DOM is static and easy to capture. Most SaaS dashboards don't need download — if they do, invest 2 hours in a screenshot button and move on.

The Bottom Line

Pick your chart library based on your ceiling, not your average. D3 has unlimited customization — use it when you're breaking new ground in data storytelling. Victory is a solid middle ground — use it if your team already bought the prop-drilling model. Recharts is the fastest path to a polished dashboard: declarative JSX, responsive by default, CSS variable theming, and zero boilerplate. Velocity X's dashboards (Rebuild Relief staff tool, Staff Operations Dashboard analytics) ship on Recharts because the code stays boring and the UX stays smooth. Pair it with custom dashboard builds where your data visualization strategy matters more than the rendering library, and you've got a system that scales from day 1 to Series B. Ship the chart in an afternoon. Spend the week on the insights instead.

Let us make some quick suggestions?

Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.