Create a Pie Chart Using Recharts in ReactJS

When I build dashboards for real teams—product managers tracking feature adoption, sales leads watching pipeline mix, or ops folks monitoring incident categories—someone always asks for “a quick pie chart.” It’s deceptively simple: a circle sliced into meaningful portions. But there’s nuance. You need reliable labels, accessible colors, sensible tooltips, and interaction that doesn’t feel twitchy. A pie chart that looks good in a design mock can mislead users if you pick the wrong keys, mis-handle small slices, or forget to handle empty data.

In this walkthrough, I’ll show you exactly how I create a pie chart in React using Recharts. I’ll start with the simplest working version, then make it more production-ready: hover states, custom colors, legend behavior, responsive containers, accessibility improvements, and a strategy for dealing with long labels or tiny values. I’ll also call out when a pie chart is the wrong choice and how to make that call quickly. You’ll walk away with a runnable example and a set of practical patterns you can reuse across projects.

Why I reach for Recharts

Recharts is a React charting library built on D3 concepts but designed for React’s component model. That combination matters: I can compose a chart like any other component, keep data structures simple, and avoid the “imperative D3 in a React app” friction. I recommend Recharts when you want clean defaults, a strong API, and fast results without writing custom SVG logic.

Recharts isn’t the only option in 2026. There are excellent choices like ECharts, Chart.js, and Vega-Lite. But for teams already in React and looking for a pleasant developer experience, Recharts is still a solid choice. It handles pie charts, line charts, bar charts, composed charts, and more with a consistent API, and it plays well with TypeScript and modern React patterns.

When a pie chart is the right choice (and when it isn’t)

I use pie charts for category share at a single point in time—“percentage of total” is the right mental model. If your data answers a question like “What proportion of support tickets are billing-related?” a pie chart can be effective.

However, I skip pie charts when:

  • You’re comparing many categories (more than 6–8 slices)
  • Differences are subtle (two slices are close in size)
  • Users need to compare values precisely

In those cases, I recommend a bar chart. A horizontal bar chart, in particular, is usually easier to read. I still keep a pie chart for “quick glance” dashboards where the goal is “big picture” rather than precision.

Setup: React app and dependencies

If you’re starting fresh, create a React app and install Recharts. I keep this tight and standard.

Commands:

  • npx create-react-app foldername
  • cd foldername
  • npm i --save recharts

If you’re on a modern React setup with Vite or Next.js, Recharts still works with minimal changes. I’ll stick to the classic setup for clarity because it’s the most straightforward to follow and easy to port.

Here’s a typical dependency set that works well:

"dependencies": {

"react": "^18.2.0",

"react-dom": "^18.2.0",

"react-scripts": "5.0.1",

"recharts": "^2.9.3"

}

Build the simplest pie chart first

I always start with a basic, working chart. That lets me verify data shape and make sure the chart renders. Then I layer in interaction and styling. Here’s a full example you can paste into App.js:

import React from "react";

import { PieChart, Pie, Tooltip, Cell } from "recharts";

const data = [

{ name: "Beginner React", students: 400 },

{ name: "Full-Stack Track", students: 700 },

{ name: "UI Systems", students: 200 },

{ name: "AI Tools", students: 1000 }

];

const COLORS = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042"];

export default function App() {

return (

<Pie

data={data}

dataKey="students"

outerRadius={250}

fill="#00C49F"

>

{data.map((entry, index) => (

<Cell key={cell-${index}} fill={COLORS[index % COLORS.length]} />

))}

);

}

What matters here:

  • data is an array of objects
  • dataKey identifies the numeric property for the slice size
  • Cell lets me color slices individually
  • Tooltip gives instant interactivity without extra code

This is enough to see a pie chart. But it isn’t ready for production: no hover, no responsive layout, no accessibility considerations, and no label handling. Let’s fix that.

Add hover interaction and polish

Hover states help users connect slice color to its meaning, especially when the labels are hidden or small. In Recharts, activeIndex and onMouseEnter make this trivial.

Here’s a version with hover and a cursor-friendly UI:

import React, { useState } from "react";

import { PieChart, Pie, Tooltip, Cell } from "recharts";

const data = [

{ name: "Beginner React", students: 400 },

{ name: "Full-Stack Track", students: 700 },

{ name: "UI Systems", students: 200 },

{ name: "AI Tools", students: 1000 }

];

const COLORS = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042"];

export default function App() {

const [activeIndex, setActiveIndex] = useState(-1);

const onPieEnter = (_, index) => {

setActiveIndex(index);

};

return (

<Pie

activeIndex={activeIndex}

data={data}

dataKey="students"

outerRadius={250}

onMouseEnter={onPieEnter}

style={{ cursor: "pointer", outline: "none" }}

>

{data.map((entry, index) => (

<Cell key={cell-${index}} fill={COLORS[index % COLORS.length]} />

))}

);

}

I keep activeIndex in state and let Recharts handle the highlight. The cursor style is small but important; it tells users that the chart is interactive. I also remove the outline on focus because the default outline can look odd with SVG elements. If your app has strict accessibility requirements, you’ll want a more deliberate focus treatment—more on that later.

Make it responsive the right way

Static width/height works for demos, but real dashboards need to adapt. I use ResponsiveContainer to let the chart size itself based on the parent. This prevents clipping and makes the chart usable on smaller screens.

import React, { useState } from "react";

import { ResponsiveContainer, PieChart, Pie, Tooltip, Cell } from "recharts";

const data = [

{ name: "Beginner React", students: 400 },

{ name: "Full-Stack Track", students: 700 },

{ name: "UI Systems", students: 200 },

{ name: "AI Tools", students: 1000 }

];

const COLORS = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042"];

export default function App() {

const [activeIndex, setActiveIndex] = useState(-1);

return (

<Pie

activeIndex={activeIndex}

data={data}

dataKey="students"

outerRadius="80%"

onMouseEnter={(_, index) => setActiveIndex(index)}

style={{ cursor: "pointer" }}

>

{data.map((entry, index) => (

<Cell key={cell-${index}} fill={COLORS[index % COLORS.length]} />

))}

);

}

Two key changes:

  • ResponsiveContainer wraps the chart
  • outerRadius uses a percentage so it scales with container size

I set the container height explicitly; otherwise, the chart collapses. This is a common mistake—Recharts needs a height from the DOM. I set it on a parent div and let width be 100%.

Add labels without clutter

Labels on a pie chart can quickly become messy. For small slices or long names, labels overlap and become unreadable. My pattern: show labels only for slices above a certain threshold and show the rest via tooltip.

Here’s a custom label renderer:

const renderLabel = ({ name, percent }) => {

if (percent < 0.08) return null; // hide tiny slices

return ${name} (${(percent * 100).toFixed(0)}%);

};

Then I attach it:

<Pie

data={data}

dataKey="students"

outerRadius="80%"

label={renderLabel}

>

This is where a simple analogy helps: I think of labels like street signs. A few well-placed signs keep you oriented; too many make it harder to navigate. By hiding labels for very small slices, you reduce visual noise while still preserving access to details through tooltips.

If you want consistent labeling, I suggest using a custom legend instead of on-slice labels. In practice, legends are easier to scan and don’t fight for space.

Improve tooltips for clarity

Default tooltips are fine, but I often want to add a percentage or format numbers. I use a custom tooltip component for clarity and a more polished look.

const CustomTooltip = ({ active, payload }) => {

if (!active |

!payload

!payload.length) return null;

const { name, value, percent } = payload[0];

return (

<div style={{

background: "white",

border: "1px solid #ddd",

padding: "8px 12px",

borderRadius: 6,

boxShadow: "0 2px 6px rgba(0,0,0,0.1)"

}}>

{name}

{value.toLocaleString()} students
{(percent * 100).toFixed(1)}% of total

);

};

Then:

<Tooltip content={} />

This makes the chart more useful because it answers two questions at once: absolute value and share. I also format the number with toLocaleString() so large values are readable. Those little touches improve trust and reduce cognitive load.

Use colors that hold up in real UI

Colors can make or break a pie chart. I choose palettes that are distinct, accessible, and consistent across the app. If you’re not sure, a good fallback is to use a known palette or a quick generator, then manually adjust for contrast.

My rules of thumb:

  • Avoid pure red/green pairs for accessibility
  • Keep saturation consistent so no slice feels “more important” by accident
  • Use a neutral background to avoid color distortion

Here’s a palette I like for dashboards:

const COLORS = ["#2D7FF9", "#24C78E", "#FFC857", "#FF7A59", "#9B6BFF", "#4ECDC4"];

If you have more categories than colors, use modulo indexing but keep the slice ordering stable so colors don’t shuffle across renders. I also recommend keeping color assignments stable across pages, so “Revenue” is always blue, “Costs” always orange, and so on.

The production-ready example

Now I’ll combine the key pieces: responsive layout, hover, custom tooltip, label filtering, and polished styling. This is the version I’d ship in a typical dashboard.

import React, { useState, useMemo } from "react";

import {

ResponsiveContainer,

PieChart,

Pie,

Tooltip,

Cell

} from "recharts";

const rawData = [

{ name: "Beginner React", students: 400 },

{ name: "Full-Stack Track", students: 700 },

{ name: "UI Systems", students: 200 },

{ name: "AI Tools", students: 1000 },

{ name: "Data Viz", students: 150 }

];

const COLORS = ["#2D7FF9", "#24C78E", "#FFC857", "#FF7A59", "#9B6BFF"];

const CustomTooltip = ({ active, payload }) => {

if (!active |

!payload

!payload.length) return null;

const { name, value, percent } = payload[0];

return (

<div style={{

background: "white",

border: "1px solid #ddd",

padding: "8px 12px",

borderRadius: 6,

boxShadow: "0 2px 6px rgba(0,0,0,0.1)"

}}>

{name}

{value.toLocaleString()} students
{(percent * 100).toFixed(1)}% of total

);

};

export default function App() {

const [activeIndex, setActiveIndex] = useState(-1);

// Example of a small preprocessing step that could be extended later

const data = useMemo(() => rawData.filter(d => d.students > 0), []);

const renderLabel = ({ name, percent }) => {

if (percent < 0.08) return null;

return ${name} (${(percent * 100).toFixed(0)}%);

};

return (

<Pie

activeIndex={activeIndex}

data={data}

dataKey="students"

outerRadius="78%"

label={renderLabel}

onMouseEnter={(_, index) => setActiveIndex(index)}

style={{ cursor: "pointer" }}

>

{data.map((entry, index) => (

<Cell key={cell-${index}} fill={COLORS[index % COLORS.length]} />

))}

<Tooltip content={} />

);

}

This is complete and runnable. If you paste it into your App.js, it will render a chart that behaves nicely on most screens, reads well, and handles small slices gracefully.

Common mistakes I see (and how I avoid them)

1) Wrong dataKey: If the key doesn’t match a numeric field, the chart renders with zero values. I always log data once and check the keys.

2) Missing container height: Without a height, the chart collapses. I wrap it in a div with a fixed height or use CSS utilities.

3) Too many slices: If there are more than 8 categories, I group the smallest ones into “Other.” This reduces clutter and improves readability.

4) Color mismatch across re-renders: If the data ordering changes, colors will shift. I sort data or map colors by category name so colors stay consistent.

5) Labels everywhere: Labels make the chart unreadable for small slices. I filter or use a legend instead.

These issues are easy to miss in a quick demo but show up fast in production. I keep a small checklist for myself whenever I add a pie chart to a live dashboard.

Practical performance considerations

Pie charts are light, but there are still performance and UX considerations:

  • Recharts rendering usually stays snappy for 10–20 slices, typically within 10–15ms on modern laptops.
  • Tooltips and hover effects add minimal overhead, but re-rendering large datasets can become noticeable.
  • If you update data frequently (every second), consider memoizing data and chart props to avoid unnecessary re-renders.
  • Animations are attractive but can be distracting. I turn them off for data that updates rapidly.

If you ever profile and find the chart causing UI jank, you can memoize the chart component or reduce the update frequency. Most dashboards don’t need real-time updates for a pie chart.

Accessibility notes I actually use

SVG charts are not automatically accessible. I take a few basic steps:

  • Provide a text summary of the data near the chart
  • Ensure tooltip content is available through other means for keyboard users
  • Use high-contrast colors and avoid relying on color alone
  • Add an aria-label for the chart container
  • Use a legend or list that can be read by screen readers

Here’s an example of a simple text summary next to the chart:

Enrollment breakdown: Beginner React 400, Full-Stack Track 700, UI Systems 200, AI Tools 1000.

If you’re building for strict accessibility requirements, I recommend pairing the chart with a data table or list view. It doesn’t have to be huge—just enough for a screen reader to capture the data accurately.

Data shape and preprocessing patterns

The easiest way to break a pie chart is to pass in data that isn’t clean. In production, datasets often include zeros, nulls, or values you don’t want represented. I almost always add a preprocessing step, even if it’s small.

Here’s a pattern I use to normalize data:

const normalizeData = (rows) => {

const safeRows = rows

.filter(r => typeof r.value === "number" && r.value > 0)

.map(r => ({ …r, label: r.label.trim() }));

const total = safeRows.reduce((sum, r) => sum + r.value, 0);

return { safeRows, total };

};

Why this matters:

  • Filter out invalid values so the chart doesn’t render “ghost slices.”
  • Trim labels so your legend doesn’t show awkward whitespace.
  • Precompute totals so you can use them in tooltips or summary text.

I treat this as part of the chart component contract: if the chart is given messy input, it’s more likely to mislead users.

Handling empty and near-empty datasets

Empty data happens more often than you think: a new tenant, a filter that removes all categories, or a backend issue that returns no rows. You should explicitly handle these cases instead of rendering a blank circle.

Here’s a pattern I like:

if (!data.length) {

return (

No data available for this period.

);

}

For near-empty data—like a single category taking 98% of the total—I tend to add a threshold rule to hide labels for tiny slices and group the smallest into “Other.” That prevents the chart from being 90% label noise for 2% of the value.

Grouping small slices into “Other”

This is one of the most practical improvements for real dashboards. If you have many categories, some slices are too small to read. Grouping them into “Other” keeps the chart readable and still preserves the total.

Here’s a simple approach:

const groupSmallSlices = (rows, minPercent = 0.05) => {

const total = rows.reduce((sum, r) => sum + r.value, 0);

const large = [];

let otherValue = 0;

rows.forEach(r => {

if (r.value / total >= minPercent) {

large.push(r);

} else {

otherValue += r.value;

}

});

if (otherValue > 0) {

large.push({ name: "Other", value: otherValue });

}

return large;

};

I usually pick a threshold around 3–5% depending on the audience. Finance teams might want more detail, while executive dashboards favor simplicity. The key is to keep the chart legible while still respecting the data.

Make legends actually useful

Legends are often an afterthought, but they’re how many people read a pie chart. I like to customize them so they include both label and value. Recharts lets you do this with a custom legend component.

import { Legend } from "recharts";

const CustomLegend = ({ payload }) => {

return (

    {payload.map((entry, index) => (

    <li key={item-${index}} style={{ display: "flex", alignItems: "center", marginBottom: 6 }}>
    <span style={{

    width: 10,

    height: 10,

    background: entry.color,

    display: "inline-block",

    borderRadius: 2,

    marginRight: 8

    }} />

    {entry.value}

    ))}

);

};

Then use it:

<Legend content={} />

If you want the legend to include numbers, you can pass extra info by mapping your data before rendering. In many cases, I generate the legend data explicitly so the order and labels are exactly what I expect.

Maintain stable colors across sorting and filters

One of the most subtle problems with pie charts is color drift. If you sort data by value or apply filters, slice order changes, and colors shift. The result: “Marketing” might be blue on one day and orange on another, confusing users.

I solve this by mapping colors to category names:

const COLOR_MAP = {

"Beginner React": "#2D7FF9",

"Full-Stack Track": "#24C78E",

"UI Systems": "#FFC857",

"AI Tools": "#FF7A59",

"Data Viz": "#9B6BFF"

};

const getColor = (name) => COLOR_MAP[name] || "#999999";

Then in the chart:

This is less flashy but much more trustworthy. Users learn to associate a color with a category, and they shouldn’t have to re-learn that each time.

Handling long labels and text overflow

Long category names are a reality in real products. “Payments & Billing Issues (Enterprise)” doesn’t fit nicely on a slice label. You have a few options:

  • Truncate labels on slices and show the full label in the tooltip.
  • Move labels to a legend where text can wrap.
  • Use a custom label renderer that adds a second line or smaller font size.

Here’s a simple truncation helper:

const truncate = (text, max = 16) => {

if (text.length <= max) return text;

return text.slice(0, max – 1) + "…";

};

And in the label:

const renderLabel = ({ name, percent }) => {

if (percent < 0.08) return null;

return ${truncate(name, 14)} (${(percent * 100).toFixed(0)}%);

};

This keeps the chart clean and pushes the full label to the tooltip where there’s space to read it.

Adding a center label or total

Sometimes I want to show a total in the center of the pie chart. This works especially well when the pie chart represents the breakdown of a larger number (like total users or total revenue). Recharts can handle a custom label in the center.

const renderCenterLabel = ({ cx, cy, total }) => {

return (

{total.toLocaleString()}

);

};

Then inside the chart:

const total = data.reduce((sum, d) => sum + d.students, 0);

<Pie

data={data}

dataKey="students"

outerRadius="78%"

label={renderLabel}

>

{renderCenterLabel({ cx: 210, cy: 210, total })}

You’ll need to compute cx and cy based on your chart dimensions. Another approach is to add an overlaying

positioned over the chart center. That’s often easier and more predictable in responsive layouts.

Example: controlled animation strategy

Animations can make charts feel alive, but they can also make data feel unstable if it updates frequently. I use a simple rule: animate on first render, then disable animation for subsequent updates.

import React, { useRef } from "react";

const useAnimateOnce = () => {

const hasAnimated = useRef(false);

if (!hasAnimated.current) {

hasAnimated.current = true;

return true;

}

return false;

};

Then:

const animate = useAnimateOnce();

This keeps the initial load feeling smooth but avoids distracting animation on every data refresh.

Keyboard interaction and focus handling

Most pie chart interactions are mouse-first. If you need keyboard support, you have to explicitly add it. I do this in two steps:

1) Provide a list-based legend that users can tab through.

2) Update the active slice when a legend item is focused.

Here’s a very simple approach:

const [activeIndex, setActiveIndex] = useState(-1);

const LegendList = ({ items }) => (

    {items.map((item, index) => (

  • ))}

);

This doesn’t make the pie itself focusable, but it gives keyboard users a way to explore slices and see the tooltip content update if you sync it with activeIndex.

Troubleshooting: chart doesn’t render or looks wrong

When something breaks, I debug in a short sequence:

1) Check the data shape. Log the data array and verify dataKey matches a number.

2) Confirm container height. If you’re using ResponsiveContainer, make sure the parent has height.

3) Remove custom renderers. If the chart appears when you remove labels/tooltip, the issue is in your custom code.

4) Reduce to a minimal dataset. Try a 2-slice chart to see if the problem persists.

5) Verify CSS. Sometimes a parent container has display: none or zero height.

This sequence solves 90% of the “nothing shows” problems.

Alternative approaches: pie vs doughnut vs stacked bar

Sometimes the best solution isn’t a pie chart at all. Here’s how I decide:

  • Pie chart: When you have 3–6 categories and want an immediate proportion view.
  • Doughnut chart: When you want a center label or a bit more whitespace for labels.
  • Stacked bar: When you need precise comparisons and a more scalable layout.

A doughnut chart is the same as a pie chart with an inner radius. In Recharts, that’s just innerRadius on the Pie component.

I use doughnuts when I want to show a total in the center or avoid the “heavy” look of a full pie chart.

Practical scenarios where this shines

Here are a few scenarios where the patterns above have paid off for me:

  • Support ticket categories: Show the top 5 categories and group the rest into “Other.” The legend lists counts and percentages.
  • Feature adoption mix: Pie chart on a dashboard with a small footnote that the data is for the last 30 days.
  • Sales pipeline by stage: Useful for a quick view, but I pair it with a stacked bar for precise comparison.
  • Operational incident types: Small slices are hidden from labels, but all data appears in tooltip and a table below.

In each case, the trick is to use the pie chart for quick intuition and provide alternative text or tables for precise values.

Using TypeScript with Recharts

If you’re in a TypeScript codebase, you can improve the reliability of your chart with a simple type for the data:

type PieDatum = {

name: string;

students: number;

};

const data: PieDatum[] = [

{ name: "Beginner React", students: 400 },

{ name: "Full-Stack Track", students: 700 }

];

This catches common issues like students being a string from an API response. You can also add a preprocessing step that ensures numbers are converted correctly:

const toNumber = (v) => (typeof v === "string" ? Number(v) : v);

const normalized = rawData.map(d => ({

…d,

students: toNumber(d.students)

}));

Subtle UX improvements that add trust

Small improvements can make a pie chart feel more “finished.” Here are a few I use regularly:

  • Consistent ordering: Sort categories by value or name and keep that stable across filters.
  • Hover fade: Reduce opacity for non-active slices to focus attention.
  • Legend alignment: Place the legend close to the chart, not far away.
  • Whitespace: Give the chart a little padding so labels and tooltips don’t feel cramped.

Here’s a quick hover fade idea using activeIndex:

<Cell

key={cell-${index}}

fill={COLORS[index % COLORS.length]}

opacity={activeIndex === -1 || activeIndex === index ? 1 : 0.5}

/>

This makes hover interaction clearer without being flashy.

Production-ready checklist

Before I ship a pie chart, I run through this checklist:

  • Data: numeric values are valid and non-negative
  • Labels: visible only for meaningful slices
  • Tooltips: include both value and percentage
  • Colors: stable across views and accessible
  • Responsiveness: chart scales and doesn’t overflow
  • Empty state: clear message if no data
  • Accessibility: text summary or table available

This takes only a few minutes and saves a lot of support questions later.

Final thoughts

A pie chart in React isn’t hard, but a good pie chart takes care. Recharts gives you a strong baseline, but the final quality comes from the details: data cleaning, label management, stable color mapping, and clear tooltips. Those details matter because users will make decisions based on what they see.

If you keep the chart focused—small number of categories, stable colors, meaningful labels—it can be a powerful quick-glance tool. If the data is more complex, use a different chart and save the pie for what it does best: a clean, immediate sense of proportions.

That’s my approach. Start simple, add depth only where it improves clarity, and make sure your chart is telling the truth as well as it looks good doing it.

Expansion Strategy

Add new sections or deepen existing ones with:

  • Deeper code examples: More complete, real-world implementations
  • Edge cases: What breaks and how to handle it
  • Practical scenarios: When to use vs when NOT to use
  • Performance considerations: Before/after comparisons (use ranges, not exact numbers)
  • Common pitfalls: Mistakes developers make and how to avoid them
  • Alternative approaches: Different ways to solve the same problem

If Relevant to Topic

  • Modern tooling and AI-assisted workflows (for infrastructure/framework topics)
  • Comparison tables for Traditional vs Modern approaches
  • Production considerations: deployment, monitoring, scaling
Scroll to Top