Building a Beautiful Interactive Pie Chart in React with Recharts and Tailwind CSS
A practical tutorial for building a production-ready pie chart in React with hover selection, animated pointer, click interactions, and shadcn/ui-compatible styling using Recharts and Tailwind CSS.
Recharts is the most popular charting library in the React ecosystem for good reason — it is declarative, composable, and built on SVG. The problem is that Recharts' default styles look like a 2018 dashboard with none of the visual consistency your shadcn/ui components have established.
This tutorial builds a production-ready pie chart from scratch: padded sectors, hover selection, an animated triangle pointer that tracks the active slice, and a center detail panel that displays the selected category's data. At the end, you can skip directly to the pre-built version from tent ui.
Install dependencies
Recharts does not require a Framer Motion dependency — the animated pointer is built with CSS transitions and SVG transforms.
The basic pie chart
Recharts renders a pie chart through composable components. The minimal version looks like this:
import { PieChart, Pie, Cell, Tooltip } from "recharts";
const data = [
{ name: "Design", value: 400 },
{ name: "Engineering", value: 300 },
{ name: "Marketing", value: 200 },
{ name: "Operations", value: 100 }
];
const COLORS = ["#0ea5e9", "#8b5cf6", "#f59e0b", "#10b981"];
export function BasicPieChart() {
return (
<PieChart width={300} height={300}>
<Pie
data={data}
cx={150}
cy={150}
innerRadius={60}
outerRadius={100}
paddingAngle={3}
dataKey="value"
>
{data.map((entry, index) => (
<Cell
key={entry.name}
fill={COLORS[index % COLORS.length]}
/>
))}
</Pie>
<Tooltip />
</PieChart>
);
}paddingAngle={3} creates the gaps between sectors — this is the "padded sector" style. innerRadius makes it a donut chart, which gives you space for a center detail panel.
Making it responsive
Hard-coded width and height break on different screen sizes. Use Recharts' ResponsiveContainer to let the chart fill its parent:
import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts";
export function ResponsivePieChart() {
return (
<div className="h-64 w-full">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
innerRadius="50%"
outerRadius="80%"
paddingAngle={3}
dataKey="value"
>
{data.map((entry, index) => (
<Cell key={entry.name} fill={COLORS[index]} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
);
}Using percentage values for cx, cy, innerRadius, and outerRadius scales the chart proportionally with its container.
Hover and click selection
Recharts' activeIndex prop highlights the active sector. Wire it to state to track selection:
"use client";
import { useState } from "react";
import { PieChart, Pie, Cell, ResponsiveContainer, Sector } from "recharts";
export function SelectablePieChart() {
const [activeIndex, setActiveIndex] = useState<number | null>(null);
function renderActiveShape(props: any) {
const {
cx, cy, innerRadius, outerRadius,
startAngle, endAngle, fill
} = props;
return (
<g>
<Sector
cx={cx}
cy={cy}
innerRadius={innerRadius - 4}
outerRadius={outerRadius + 6}
startAngle={startAngle}
endAngle={endAngle}
fill={fill}
/>
</g>
);
}
return (
<ResponsiveContainer width="100%" height={280}>
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
innerRadius="50%"
outerRadius="80%"
paddingAngle={3}
dataKey="value"
activeIndex={activeIndex ?? undefined}
activeShape={renderActiveShape}
onMouseEnter={(_, index) => setActiveIndex(index)}
onMouseLeave={() => setActiveIndex(null)}
onClick={(_, index) => setActiveIndex(index)}
>
{data.map((entry, index) => (
<Cell
key={entry.name}
fill={COLORS[index]}
opacity={
activeIndex === null || activeIndex === index ? 1 : 0.6
}
className="cursor-pointer"
/>
))}
</Pie>
</PieChart>
</ResponsiveContainer>
);
}The renderActiveShape function expands the active sector — innerRadius - 4 and outerRadius + 6 grow the slice outward in both directions. The opacity on non-active cells creates the dimming effect that focuses attention on the selected slice.
The center detail panel
The space inside a donut chart is valuable real estate for displaying the selected item's data. Recharts does not have a built-in center label, so position one with absolute CSS inside ResponsiveContainer:
export function DonutWithCenter() {
const [activeIndex, setActiveIndex] = useState<number>(0);
const activeItem = data[activeIndex];
const total = data.reduce((sum, d) => sum + d.value, 0);
const percentage = Math.round((activeItem.value / total) * 100);
return (
<div className="relative h-64 w-full">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
innerRadius="52%"
outerRadius="78%"
paddingAngle={3}
dataKey="value"
onMouseEnter={(_, index) => setActiveIndex(index)}
>
{data.map((entry, index) => (
<Cell
key={entry.name}
fill={COLORS[index]}
opacity={activeIndex === index ? 1 : 0.65}
/>
))}
</Pie>
</PieChart>
</ResponsiveContainer>
{/* Center label — absolutely positioned over the chart */}
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center">
<span className="text-2xl font-bold tabular-nums">
{percentage}%
</span>
<span className="text-muted-foreground text-xs">
{activeItem.name}
</span>
</div>
</div>
);
}The pointer-events-none class prevents the overlay from intercepting mouse events that should reach the SVG chart below it.
Matching shadcn/ui color conventions
To align the chart with your app's design tokens, use CSS custom properties from your Tailwind/shadcn config instead of hard-coded hex values:
const COLORS = [
"hsl(var(--chart-1))",
"hsl(var(--chart-2))",
"hsl(var(--chart-3))",
"hsl(var(--chart-4))",
"hsl(var(--chart-5))"
];shadcn/ui v4 ships --chart-1 through --chart-5 as CSS variables. They adapt to light and dark mode automatically. Use these for any Recharts component to get visual consistency with the rest of your component library.
Adding a legend
An accessible legend pairs with the chart for users who cannot distinguish colors (approximately 8% of males have color blindness):
import { Legend } from "recharts";
function CustomLegend({ payload }: any) {
return (
<ul className="flex flex-wrap justify-center gap-x-4 gap-y-1 text-xs">
{payload.map((entry: any, index: number) => (
<li key={entry.value} className="flex items-center gap-1.5">
<span
className="inline-block h-2 w-2 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<span className="text-muted-foreground">{entry.value}</span>
</li>
))}
</ul>
);
}
// Inside the PieChart:
<Legend content={<CustomLegend />} verticalAlign="bottom" />The custom legend renderer gives you full control over the layout and styling, so it looks like part of your UI rather than a Recharts widget.
Skip the implementation
The production-ready version with the animated pointer, center detail panel, legend, dark mode, and hover/click interactions — all using your shadcn color tokens — is available to install directly:
The component lands as editable source in your project. See the full documentation with interactive demo at /docs/components/pie-chart.
For geographic data visualization, see World Map. For form components that complete a dashboard's utility surface, see Password Input and Inline Edit.