DEV Community

Carlos Oliva Pascual
Carlos Oliva Pascual

Posted on • Originally published at stacknotice.com

Recharts: React Data Visualization Complete Guide (2026)

Chart libraries for React split into two categories: those that are React-native (data flows in as props, components compose naturally) and those ported from JavaScript that fight React's model. Recharts is in the first category — built on top of SVG and D3, but exposed as React components you compose together.

This guide covers every chart type and pattern you'd actually need in a dashboard or analytics interface: time-series lines, comparison bars, trend areas, distributions, custom tooltips, dark mode, and real data shapes.

Installation

npm install recharts
Enter fullscreen mode Exit fullscreen mode
import {
  LineChart, BarChart, AreaChart, PieChart,
  Line, Bar, Area, Pie, Cell,
  XAxis, YAxis, CartesianGrid, Tooltip, Legend,
  ResponsiveContainer,
} from 'recharts'
Enter fullscreen mode Exit fullscreen mode

ResponsiveContainer: Always Use It

Never hardcode width and height on chart components directly. ResponsiveContainer makes charts fill their container and reflow on resize:

// ❌ Fixed size — breaks in sidebars, modals, responsive layouts
<LineChart width={600} height={300} data={data} />

// ✅ Fills container, adapts to screen size
<ResponsiveContainer width="100%" height={300}>
  <LineChart data={data}>
    {/* ... */}
  </LineChart>
</ResponsiveContainer>
Enter fullscreen mode Exit fullscreen mode

Line Chart: Time Series Data

The workhorse of analytics dashboards:

type DailyMetric = {
  date: string      // "2026-06-01"
  visitors: number
  signups: number
  revenue: number
}

function VisitorsChart({ data }: { data: DailyMetric[] }) {
  return (
    <ResponsiveContainer width="100%" height={300}>
      <LineChart data={data} margin={{ top: 5, right: 20, left: 0, bottom: 5 }}>
        <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.06)" />
        <XAxis
          dataKey="date"
          tick={{ fontSize: 12, fill: '#94a3b8' }}
          tickFormatter={(value) => new Date(value).toLocaleDateString('en', { month: 'short', day: 'numeric' })}
          axisLine={false}
          tickLine={false}
        />
        <YAxis
          tick={{ fontSize: 12, fill: '#94a3b8' }}
          axisLine={false}
          tickLine={false}
          tickFormatter={(value) => value >= 1000 ? `${(value / 1000).toFixed(1)}k` : value}
        />
        <Tooltip content={<CustomTooltip />} />
        <Legend wrapperStyle={{ fontSize: 13, paddingTop: 16 }} />

        <Line
          type="monotone"
          dataKey="visitors"
          stroke="#38BDF8"
          strokeWidth={2}
          dot={false}
          activeDot={{ r: 5, strokeWidth: 0 }}
          name="Visitors"
        />
        <Line
          type="monotone"
          dataKey="signups"
          stroke="#34D399"
          strokeWidth={2}
          dot={false}
          activeDot={{ r: 5, strokeWidth: 0 }}
          name="Sign-ups"
        />
      </LineChart>
    </ResponsiveContainer>
  )
}
Enter fullscreen mode Exit fullscreen mode

type="monotone" gives you smooth curves. type="linear" gives straight lines between points. type="step" is useful for state transitions.

Custom Tooltip

The default tooltip is functional but ugly. A custom tooltip matches your design system:

import { TooltipProps } from 'recharts'

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

  return (
    <div className="rounded-lg border border-border bg-card px-3 py-2 shadow-lg">
      <p className="mb-2 text-xs text-muted-foreground">
        {new Date(label).toLocaleDateString('en', {
          weekday: 'short', month: 'short', day: 'numeric',
        })}
      </p>
      {payload.map((entry) => (
        <div key={entry.dataKey} className="flex items-center gap-2 text-sm">
          <span
            className="inline-block h-2 w-2 rounded-full"
            style={{ backgroundColor: entry.color }}
          />
          <span className="text-muted-foreground">{entry.name}:</span>
          <span className="font-medium tabular-nums">
            {typeof entry.value === 'number'
              ? entry.value.toLocaleString()
              : entry.value}
          </span>
        </div>
      ))}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Bar Chart: Comparisons

type CategoryRevenue = {
  category: string
  current: number
  previous: number
}

function RevenueComparison({ data }: { data: CategoryRevenue[] }) {
  return (
    <ResponsiveContainer width="100%" height={280}>
      <BarChart data={data} barCategoryGap="25%" barGap={4}>
        <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="rgba(255,255,255,0.06)" />
        <XAxis dataKey="category" tick={{ fontSize: 12, fill: '#94a3b8' }} axisLine={false} tickLine={false} />
        <YAxis
          tick={{ fontSize: 12, fill: '#94a3b8' }}
          axisLine={false}
          tickLine={false}
          tickFormatter={(v) => `$${(v / 1000).toFixed(0)}k`}
        />
        <Tooltip content={<CustomTooltip />} cursor={{ fill: 'rgba(255,255,255,0.04)' }} />
        <Legend wrapperStyle={{ fontSize: 13, paddingTop: 12 }} />

        <Bar dataKey="previous" fill="#334155" radius={[4, 4, 0, 0]} name="Last month" />
        <Bar dataKey="current" fill="#38BDF8" radius={[4, 4, 0, 0]} name="This month" />
      </BarChart>
    </ResponsiveContainer>
  )
}
Enter fullscreen mode Exit fullscreen mode

radius={[4, 4, 0, 0]} rounds the top corners. barCategoryGap controls spacing between groups, barGap between bars within a group.

Horizontal bar chart

// layout="vertical" flips the axes
<BarChart layout="vertical" data={data}>
  <XAxis type="number" tick={{ fontSize: 12 }} tickFormatter={(v) => `${v}%`} />
  <YAxis type="category" dataKey="name" width={100} tick={{ fontSize: 12 }} />
  <Bar dataKey="value" fill="#38BDF8" radius={[0, 4, 4, 0]} />
</BarChart>
Enter fullscreen mode Exit fullscreen mode

Area Chart: Trends with Fill

Area charts work well for cumulative metrics or when you want to emphasize the volume under the curve:

function RevenueAreaChart({ data }: { data: DailyMetric[] }) {
  return (
    <ResponsiveContainer width="100%" height={250}>
      <AreaChart data={data} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
        <defs>
          {/* Gradient fill — fades from color to transparent */}
          <linearGradient id="revenueGradient" x1="0" y1="0" x2="0" y2="1">
            <stop offset="5%" stopColor="#38BDF8" stopOpacity={0.3} />
            <stop offset="95%" stopColor="#38BDF8" stopOpacity={0} />
          </linearGradient>
        </defs>
        <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.06)" vertical={false} />
        <XAxis dataKey="date" tick={{ fontSize: 12, fill: '#94a3b8' }} axisLine={false} tickLine={false}
          tickFormatter={(v) => new Date(v).toLocaleDateString('en', { month: 'short', day: 'numeric' })} />
        <YAxis tick={{ fontSize: 12, fill: '#94a3b8' }} axisLine={false} tickLine={false}
          tickFormatter={(v) => `$${(v / 1000).toFixed(0)}k`} />
        <Tooltip content={<CustomTooltip />} />
        <Area
          type="monotone"
          dataKey="revenue"
          stroke="#38BDF8"
          strokeWidth={2}
          fill="url(#revenueGradient)"
          dot={false}
        />
      </AreaChart>
    </ResponsiveContainer>
  )
}
Enter fullscreen mode Exit fullscreen mode

Stacked area chart

<AreaChart data={data} stackOffset="expand">
  <Area type="monotone" dataKey="organic" stackId="1" stroke="#38BDF8" fill="#38BDF8" fillOpacity={0.4} />
  <Area type="monotone" dataKey="paid" stackId="1" stroke="#F472B6" fill="#F472B6" fillOpacity={0.4} />
  <Area type="monotone" dataKey="direct" stackId="1" stroke="#34D399" fill="#34D399" fillOpacity={0.4} />
</AreaChart>
Enter fullscreen mode Exit fullscreen mode

stackOffset="expand" normalizes to 100% (like a percentage stacked chart).

Pie Chart: Distributions

type ChannelData = { name: string; value: number; color: string }

const COLORS = ['#38BDF8', '#34D399', '#F472B6', '#FBBF24', '#A78BFA']

function TrafficSourcePie({ data }: { data: ChannelData[] }) {
  const total = data.reduce((sum, d) => sum + d.value, 0)

  return (
    <ResponsiveContainer width="100%" height={260}>
      <PieChart>
        <Pie
          data={data}
          cx="50%"
          cy="50%"
          innerRadius={70}    // donut chart — remove for filled pie
          outerRadius={110}
          paddingAngle={3}
          dataKey="value"
          stroke="none"
        >
          {data.map((entry, index) => (
            <Cell key={entry.name} fill={COLORS[index % COLORS.length]} />
          ))}
        </Pie>
        <Tooltip
          content={({ active, payload }) => {
            if (!active || !payload?.length) return null
            const item = payload[0]
            return (
              <div className="rounded-lg border bg-card px-3 py-2 shadow-lg">
                <p className="font-medium">{item.name}</p>
                <p className="text-sm text-muted-foreground">
                  {item.value?.toLocaleString()} ({((Number(item.value) / total) * 100).toFixed(1)}%)
                </p>
              </div>
            )
          }}
        />
        <Legend
          iconType="circle"
          iconSize={8}
          formatter={(value, entry: any) => (
            <span className="text-sm text-muted-foreground">
              {value} <span className="font-medium text-foreground">
                {((entry.payload.value / total) * 100).toFixed(1)}%
              </span>
            </span>
          )}
        />
      </PieChart>
    </ResponsiveContainer>
  )
}
Enter fullscreen mode Exit fullscreen mode

Composed Chart: Mixed Types

Combine bars and lines in the same chart:

import { ComposedChart, Bar, Line } from 'recharts'

function MRRChart({ data }: { data: MRRData[] }) {
  return (
    <ResponsiveContainer width="100%" height={300}>
      <ComposedChart data={data}>
        <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="rgba(255,255,255,0.06)" />
        <XAxis dataKey="month" tick={{ fontSize: 12, fill: '#94a3b8' }} axisLine={false} tickLine={false} />
        <YAxis yAxisId="left" tick={{ fontSize: 12, fill: '#94a3b8' }} axisLine={false} tickLine={false}
          tickFormatter={(v) => `$${(v / 1000).toFixed(0)}k`} />
        <YAxis yAxisId="right" orientation="right" tick={{ fontSize: 12, fill: '#94a3b8' }}
          axisLine={false} tickLine={false} tickFormatter={(v) => `${v}%`} />
        <Tooltip content={<CustomTooltip />} />
        <Legend />

        {/* MRR as bars */}
        <Bar yAxisId="left" dataKey="mrr" fill="#38BDF8" radius={[4, 4, 0, 0]} name="MRR" />

        {/* Growth rate as line on secondary axis */}
        <Line yAxisId="right" type="monotone" dataKey="growthRate"
          stroke="#34D399" strokeWidth={2} dot={false} name="Growth %" />
      </ComposedChart>
    </ResponsiveContainer>
  )
}
Enter fullscreen mode Exit fullscreen mode

Reference Lines and Areas

Mark a threshold or highlight a period:

import { ReferenceLine, ReferenceArea } from 'recharts'

<LineChart data={data}>
  {/* Horizontal threshold line */}
  <ReferenceLine y={10000} stroke="#FBBF24" strokeDasharray="4 4" label={{ value: 'Target', fill: '#FBBF24', fontSize: 12 }} />

  {/* Highlight a date range */}
  <ReferenceArea x1="2026-03-01" x2="2026-03-15" fill="rgba(251,191,36,0.05)" label={{ value: 'Campaign', fill: '#FBBF24', fontSize: 11 }} />
</LineChart>
Enter fullscreen mode Exit fullscreen mode

Dark Mode

Recharts doesn't know about your theme. Control colors explicitly, ideally via CSS variables:

const chartColors = {
  grid: 'hsl(var(--border))',
  tick: 'hsl(var(--muted-foreground))',
  line1: 'hsl(var(--primary))',
  line2: '#34D399',
}

<CartesianGrid stroke={chartColors.grid} strokeDasharray="3 3" />
<XAxis tick={{ fill: chartColors.tick }} />
<Line stroke={chartColors.line1} />
Enter fullscreen mode Exit fullscreen mode

Performance with Large Datasets

Recharts renders SVG — 10,000 data points is too many to render efficiently. Downsample your data before passing it to the chart:

function downsample<T>(data: T[], maxPoints: number): T[] {
  if (data.length <= maxPoints) return data
  const step = Math.ceil(data.length / maxPoints)
  return data.filter((_, i) => i % step === 0)
}

// Target ~500 points for a typical chart width
const chartData = downsample(rawData, 500)
Enter fullscreen mode Exit fullscreen mode

Quick Reference

// Responsive wrapper — always use
<ResponsiveContainer width="100%" height={300}>
  <LineChart data={data}>
    <CartesianGrid strokeDasharray="3 3" />
    <XAxis dataKey="date" />
    <YAxis />
    <Tooltip content={<CustomTooltip />} />
    <Legend />
    <Line type="monotone" dataKey="value" stroke="#38BDF8" strokeWidth={2} dot={false} />
  </LineChart>
</ResponsiveContainer>

// Area with gradient
<defs>
  <linearGradient id="grad" x1="0" y1="0" x2="0" y2="1">
    <stop offset="5%" stopColor="#38BDF8" stopOpacity={0.3} />
    <stop offset="95%" stopColor="#38BDF8" stopOpacity={0} />
  </linearGradient>
</defs>
<Area fill="url(#grad)" />

// Stacked area
<Area stackId="1" dataKey="a" />
<Area stackId="1" dataKey="b" />

// Donut vs pie: innerRadius on <Pie>
<Pie innerRadius={70} outerRadius={110} />  // donut
<Pie outerRadius={110} />                    // filled pie

// Horizontal bar
<BarChart layout="vertical" data={data}>
  <XAxis type="number" />
  <YAxis type="category" dataKey="name" />
</BarChart>

// Mixed chart
<ComposedChart>
  <Bar dataKey="mrr" />
  <Line dataKey="growth" />
</ComposedChart>
Enter fullscreen mode Exit fullscreen mode

The pattern worth noting: put all chart config (stroke colors, tick formatters, margin) in a shared chartConfig object so all charts in the app have consistent styling. A dark stroke for grid lines, muted text for axis ticks, and your brand color for the primary data series applied consistently across every chart makes the dashboard feel cohesive rather than assembled.


Full article at stacknotice.com/blog/recharts-react-data-visualization-2026

Top comments (0)