Pie Chart
Free, copy-pasteable Tailwind CSS Pie Chart component. Accessible, fully responsive, dark-mode ready, and customizable.
Install via CLI
Run this command to automatically add the component and its dependencies to your project.
npx @abhaysinghr516/business-wish add pie-chartnpx @abhaysinghr516/business-wish init first to initialize your project.Beautifully minimal Pie & Donut Chart components built on recharts. Every variant ships with smooth entry animations, a polished custom tooltip, and seamless light/dark mode support.
Four variants cover all common use-cases: a static donut with a progress-bar legend, an interactive hover-to-explore donut, a precision half-donut gauge, and a compact spark-pie widget card.
Basic Donut Chart
A clean donut for categorical breakdown — here, traffic sources. Features a centered total label and a side-by-side legend with inline proportional progress tracks for instant visual scanning.
"use client";
import React from "react";
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from "recharts";
import { PieChart as PieChartIcon } from "lucide-react";
const trafficSourcesData = [
{ name: "Direct", value: 35, color: "#3B82F6" },
{ name: "Organic", value: 25, color: "#6366F1" },
{ name: "Referral", value: 20, color: "#8B5CF6" },
{ name: "Social", value: 20, color: "#D946EF" },
];
const CustomTooltip = ({ active, payload }: any) => {
if (active && payload?.length) {
const d = payload[0].payload;
return (
<div className="bg-white dark:bg-neutral-900 border border-neutral-200/80 dark:border-neutral-800 p-3 rounded-2xl shadow-lg min-w-[140px]">
<div className="flex items-center gap-2.5">
<div className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: d.color }} />
<span className="text-[13px] font-medium text-neutral-500 flex-1 capitalize">{d.name}</span>
<span className="text-[13px] font-bold text-neutral-900 dark:text-white tabular-nums">{d.value}%</span>
</div>
</div>
);
}
return null;
};
export const BasicDonutChart = ({ height = 300 }) => {
const total = trafficSourcesData.reduce((s, d) => s + d.value, 0);
return (
<div className="w-full bg-white dark:bg-[#0A0A0A] border border-neutral-200 dark:border-neutral-800/80 rounded-3xl p-6 shadow-sm">
<div className="mb-6">
<div className="flex items-center gap-2 mb-1">
<PieChartIcon className="w-3.5 h-3.5 text-neutral-400" strokeWidth={2.5} />
<p className="text-[12px] font-semibold text-neutral-500 uppercase tracking-widest">Traffic Sources</p>
</div>
<h3 className="text-[22px] font-bold text-neutral-900 dark:text-white tracking-tight">Visitor Breakdown</h3>
<p className="text-[13px] text-neutral-400 mt-1">Distribution of incoming sessions</p>
</div>
<div className="flex flex-col sm:flex-row items-center gap-6">
{/* Donut */}
<div style={{ height, width: "100%", maxWidth: 280 }} className="relative flex-shrink-0">
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
<span className="text-[28px] font-bold text-neutral-900 dark:text-white tracking-tighter leading-none">{total}%</span>
<span className="text-[10px] font-semibold text-neutral-400 uppercase tracking-widest mt-1">Total</span>
</div>
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Tooltip content={<CustomTooltip />} />
<Pie
data={trafficSourcesData}
cx="50%" cy="50%"
innerRadius="58%" outerRadius="80%"
paddingAngle={2} dataKey="value" stroke="none"
animationDuration={1400} animationEasing="ease-out"
>
{trafficSourcesData.map((entry, i) => <Cell key={i} fill={entry.color} />)}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
{/* Progress-bar legend */}
<div className="flex flex-col gap-3 w-full">
{trafficSourcesData.map((entry) => {
const pct = ((entry.value / total) * 100).toFixed(0);
return (
<div key={entry.name} className="flex items-center gap-3">
<div className="w-2 h-2 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-[13px] font-medium text-neutral-600 dark:text-neutral-400 flex-1">{entry.name}</span>
<div className="flex-1 max-w-[100px] h-1 bg-neutral-100 dark:bg-neutral-800 rounded-full overflow-hidden">
<div className="h-full rounded-full" style={{ width: `${pct}%`, backgroundColor: entry.color }} />
</div>
<span className="text-[12px] font-bold text-neutral-900 dark:text-white tabular-nums w-8 text-right">{pct}%</span>
</div>
);
})}
</div>
</div>
</div>
);
};
Interactive Donut Chart
An advanced donut where hovering a segment expands it and updates the header stat live. Legend items are interactive pills that mirror the hover state — clicking or hovering any pill highlights the matching segment.
"use client";
import React, { useState } from "react";
import { PieChart, Pie, Cell, ResponsiveContainer, Sector } from "recharts";
import { Disc } from "lucide-react";
const categorySalesData = [
{ name: "Electronics", value: 450, color: "#10B981" },
{ name: "Clothing", value: 300, color: "#3B82F6" },
{ name: "Home", value: 200, color: "#F59E0B" },
{ name: "Books", value: 150, color: "#8B5CF6" },
];
const renderActiveShape = (props: any) => {
const { cx, cy, innerRadius, outerRadius, startAngle, endAngle, fill, payload, percent, value } = props;
return (
<g>
<text x={cx} y={cy - 14} textAnchor="middle" fill={fill} fontSize={15} fontWeight={700}>{payload.name}</text>
<text x={cx} y={cy + 8} textAnchor="middle" fill="#8E8E93" fontSize={13} fontWeight={600}>{value.toLocaleString()}</text>
<text x={cx} y={cy + 26} textAnchor="middle" fill="#8E8E93" fontSize={12} fontWeight={500}>{`${(percent * 100).toFixed(0)}%`}</text>
<Sector cx={cx} cy={cy} innerRadius={innerRadius} outerRadius={outerRadius + 8} startAngle={startAngle} endAngle={endAngle} fill={fill} />
<Sector cx={cx} cy={cy} startAngle={startAngle} endAngle={endAngle} innerRadius={outerRadius + 12} outerRadius={outerRadius + 15} fill={fill} opacity={0.35} />
</g>
);
};
export const InteractiveDonutChart = ({ height = 340 }) => {
const [activeIndex, setActiveIndex] = useState(0);
const total = categorySalesData.reduce((s, d) => s + d.value, 0);
return (
<div className="w-full bg-white dark:bg-[#0A0A0A] border border-neutral-200 dark:border-neutral-800/80 rounded-3xl p-6 shadow-sm">
<div className="flex items-start justify-between mb-6">
<div>
<div className="flex items-center gap-2 mb-1">
<Disc className="w-3.5 h-3.5 text-neutral-400" strokeWidth={2.5} />
<p className="text-[12px] font-semibold text-neutral-500 uppercase tracking-widest">Category Sales</p>
</div>
<h3 className="text-[22px] font-bold text-neutral-900 dark:text-white tracking-tight">Revenue Mix</h3>
<p className="text-[13px] text-neutral-400 mt-1">Hover segments to explore</p>
</div>
<div className="flex flex-col items-end">
<span className="text-[22px] font-bold tracking-tighter leading-none" style={{ color: categorySalesData[activeIndex].color }}>
{categorySalesData[activeIndex].value.toLocaleString()}
</span>
<span className="text-[11px] font-semibold text-neutral-400 mt-0.5">
{((categorySalesData[activeIndex].value / total) * 100).toFixed(0)}% of total
</span>
</div>
</div>
<div style={{ height, width: "100%" }}>
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
activeIndex={activeIndex}
activeShape={renderActiveShape}
data={categorySalesData}
cx="50%" cy="50%"
innerRadius="38%" outerRadius="58%"
dataKey="value" stroke="none"
onMouseEnter={(_: any, index: number) => setActiveIndex(index)}
animationDuration={1000}
className="cursor-pointer outline-none"
>
{categorySalesData.map((entry, i) => <Cell key={i} fill={entry.color} />)}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
{/* Interactive pill legend */}
<div className="flex flex-wrap justify-center gap-2 mt-2">
{categorySalesData.map((entry, index) => (
<button
key={entry.name}
onMouseEnter={() => setActiveIndex(index)}
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full border text-[11px] font-medium transition-all duration-150 cursor-pointer ${
activeIndex === index
? "border-transparent text-white"
: "bg-neutral-50 dark:bg-neutral-800/50 border-neutral-200 dark:border-neutral-700/60 text-neutral-500"
}`}
style={activeIndex === index ? { backgroundColor: entry.color } : {}}
>
<div className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: activeIndex === index ? "#fff" : entry.color }} />
{entry.name}
</button>
))}
</div>
</div>
);
};
Gauge Chart
A half-donut (180°) gauge for displaying a score or completion metric. The foreground arc has rounded corners (cornerRadius={8}), a static background track, and scale labels at each end.
"use client";
import React from "react";
import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts";
import { Target } from "lucide-react";
const performanceScoreData = [
{ name: "Score", value: 82, color: "#3B82F6" },
{ name: "Remaining", value: 18, color: "transparent" },
];
export const GaugeChart = ({ height = 280 }) => {
const score = performanceScoreData[0].value;
const color = performanceScoreData[0].color;
const label = score >= 90 ? "EXCELLENT" : score >= 75 ? "GREAT" : score >= 60 ? "GOOD" : "NEEDS WORK";
return (
<div className="w-full bg-[#0A0A0F] border border-white/[0.06] rounded-3xl p-6 shadow-2xl overflow-hidden">
<div className="text-center mb-2">
<div className="flex items-center justify-center gap-2 mb-1">
<Target className="w-3.5 h-3.5 text-neutral-500" strokeWidth={2.5} />
<p className="text-[12px] font-semibold text-neutral-500 uppercase tracking-widest">Performance Score</p>
</div>
<p className="text-[13px] text-neutral-500">Overall system health metric</p>
</div>
<div style={{ height, width: "100%" }} className="relative mt-[-8px]">
<div className="absolute top-[62%] left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center pointer-events-none">
<span className="text-[52px] font-bold text-white tracking-tighter leading-none">{score}</span>
<span className="text-[11px] font-bold tracking-widest mt-1.5" style={{ color }}>{label}</span>
</div>
<ResponsiveContainer width="100%" height="100%">
<PieChart>
{/* Background track */}
<Pie
data={[{ value: 100 }]}
cx="50%" cy="70%" startAngle={180} endAngle={0}
innerRadius="54%" outerRadius="68%"
dataKey="value" stroke="none" fill="rgba(255,255,255,0.06)"
isAnimationActive={false}
/>
{/* Foreground arc */}
<Pie
data={performanceScoreData}
cx="50%" cy="70%" startAngle={180} endAngle={0}
innerRadius="54%" outerRadius="68%"
dataKey="value" stroke="none" cornerRadius={8}
animationDuration={2000} animationEasing="ease-out"
>
<Cell fill={color} />
<Cell fill="transparent" />
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
<div className="flex justify-between px-8 mt-[-12px]">
<span className="text-[11px] font-semibold text-neutral-600">0</span>
<span className="text-[11px] font-semibold text-neutral-600">100</span>
</div>
</div>
);
};
Widget Spark-Pie Chart
A compact donut card designed for dashboard metric grids. Shows a large primary percentage, a mini legend breakdown, and a small donut that bleeds to the right — ideal for at-a-glance retention or conversion metrics.
"use client";
import React from "react";
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from "recharts";
import { Activity } from "lucide-react";
const widgetMetricData = [
{ name: "Active", value: 65, color: "#EC4899" },
{ name: "Inactive", value: 35 },
];
export const WidgetPieChart = () => {
const activeValue = widgetMetricData[0].value;
const inactiveValue = widgetMetricData[1].value;
const color = widgetMetricData[0].color;
return (
<div className="w-full max-w-[340px] bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-3xl p-5 shadow-sm hover:shadow-md transition-shadow duration-300 flex items-center gap-5">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 mb-2">
<Activity className="w-3.5 h-3.5 text-neutral-400" strokeWidth={2.5} />
<p className="text-[11px] font-semibold text-neutral-500 uppercase tracking-widest">Retention Rate</p>
</div>
<div className="flex items-baseline gap-1">
<span className="text-[32px] font-bold text-neutral-900 dark:text-white leading-none tracking-tighter">{activeValue}</span>
<span className="text-[16px] font-bold text-neutral-400">%</span>
</div>
<p className="text-[12px] font-medium text-neutral-400 mt-2">Active users</p>
<div className="flex items-center gap-3 mt-3">
<div className="flex items-center gap-1.5">
<div className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: color }} />
<span className="text-[11px] font-medium text-neutral-500">Active {activeValue}%</span>
</div>
<div className="flex items-center gap-1.5">
<div className="w-1.5 h-1.5 rounded-full bg-neutral-200 dark:bg-neutral-700" />
<span className="text-[11px] font-medium text-neutral-400">Inactive {inactiveValue}%</span>
</div>
</div>
</div>
<div className="h-[88px] w-[88px] flex-shrink-0">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={widgetMetricData}
cx="50%" cy="50%"
innerRadius="36%" outerRadius="50%"
dataKey="value" stroke="none" paddingAngle={2}
animationDuration={1000}
>
<Cell fill={color} />
<Cell fill="currentColor" className="text-neutral-100 dark:text-neutral-800" />
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
</div>
);
};