# Donut Chart Segments — React Chart Component

> A donut whose segments sweep in one after another; hovering a segment or its legend row thickens it and swaps the centre total for that slice.

- Category: [Charts](https://pixelpapa.com/components/charts.md)
- Tags: chart, donut, pie, svg, legend
- Dependencies: motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/charts/chart-donut-segments

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/chart-donut-segments.json"
```

The file is written to `components/pixelpapa/chart-donut-segments.tsx`. To install manually, run `npm install motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useState } from "react";
import { cn } from "@/lib/utils";

export type DonutSegment = { label: string; value: number; color?: string };

type DonutChartProps = {
  data: DonutSegment[];
  /** Caption under the centre total. */
  totalLabel?: string;
  formatValue?: (v: number) => string;
  className?: string;
};

const PALETTE = ["var(--chart-1)", "var(--chart-2)", "var(--chart-3)", "var(--chart-4)", "var(--chart-5)"];
const R = 74;
const GAP = 0.008; // fraction of the circumference left empty between segments

export function DonutChart({ data, totalLabel = "Total", formatValue = (v) => v.toLocaleString("en"), className }: DonutChartProps) {
  const reduce = useReducedMotion();
  const [active, setActive] = useState<number | null>(null);
  const total = data.reduce((s, d) => s + d.value, 0);

  let acc = 0;
  const segs = data.map((d, i) => {
    const frac = d.value / total;
    const seg = { ...d, color: d.color ?? PALETTE[i % PALETTE.length], frac, start: acc };
    acc += frac;
    return seg;
  });
  const shownValue = active === null ? total : segs[active].value;
  const shownLabel = active === null ? totalLabel : `${segs[active].label} · ${(segs[active].frac * 100).toFixed(1)}%`;

  return (
    <div className={cn("flex w-full flex-col items-center gap-6 sm:flex-row sm:gap-8", className)}>
      <div className="relative size-52 shrink-0">
        <svg
          viewBox="0 0 200 200"
          className="size-full -rotate-90"
          role="img"
          aria-label={`Donut chart. ${segs.map((s) => `${s.label} ${(s.frac * 100).toFixed(0)}%`).join(", ")}.`}
        >
          <circle cx={100} cy={100} r={R} fill="none" strokeWidth={22} className="stroke-muted" />
          {segs.map((s, i) => (
            <motion.circle
              key={s.label}
              cx={100}
              cy={100}
              r={R}
              fill="none"
              stroke={s.color}
              className="cursor-pointer"
              onPointerEnter={() => setActive(i)}
              onPointerLeave={() => setActive(null)}
              initial={reduce ? false : { pathLength: 0, pathOffset: s.start }}
              animate={{
                pathLength: Math.max(s.frac - GAP, 0.001),
                pathOffset: s.start,
                strokeWidth: active === i ? 30 : 22,
                opacity: active === null || active === i ? 1 : 0.35,
              }}
              transition={{
                pathLength: { duration: 0.8, delay: 0.1 + s.start * 0.7, ease: [0.22, 1, 0.36, 1] },
                strokeWidth: { type: "spring", stiffness: 400, damping: 26 },
                opacity: { duration: 0.2 },
              }}
            />
          ))}
        </svg>
        <div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center">
          <AnimatePresence mode="popLayout" initial={false}>
            <motion.div
              key={active ?? "total"}
              initial={{ opacity: 0, y: reduce ? 0 : 8, filter: "blur(4px)" }}
              animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
              exit={{ opacity: 0, y: reduce ? 0 : -8, filter: "blur(4px)" }}
              transition={{ duration: 0.2 }}
            >
              <p className="text-2xl font-semibold tracking-tight text-foreground tabular-nums">{formatValue(shownValue)}</p>
              <p className="mt-0.5 max-w-28 text-xs text-muted-foreground">{shownLabel}</p>
            </motion.div>
          </AnimatePresence>
        </div>
      </div>

      <ul className="w-full min-w-0 flex-1 space-y-0.5">
        {segs.map((s, i) => (
          <li key={s.label}>
            <button
              type="button"
              onPointerEnter={() => setActive(i)}
              onPointerLeave={() => setActive(null)}
              onFocus={() => setActive(i)}
              onBlur={() => setActive(null)}
              className={cn(
                "flex w-full items-center gap-3 rounded-lg px-2.5 py-2 text-left text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring",
                active === i ? "bg-muted" : "bg-transparent",
              )}
            >
              <span className="size-2.5 shrink-0 rounded-full" style={{ background: s.color }} />
              <span className="truncate text-foreground">{s.label}</span>
              <span className="ml-auto text-muted-foreground tabular-nums">{formatValue(s.value)}</span>
              <span className="w-12 text-right font-medium text-foreground tabular-nums">{(s.frac * 100).toFixed(1)}%</span>
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

const traffic: DonutSegment[] = [
  { label: "Organic search", value: 18420 },
  { label: "Direct", value: 9730 },
  { label: "Referrals", value: 6210 },
  { label: "Newsletter", value: 4380 },
  { label: "Paid social", value: 2160 },
];

export default function ChartDonutSegmentsDemo() {
  return (
    <div className="w-full max-w-xl rounded-2xl border border-border bg-card p-6 shadow-sm">
      <p className="font-medium text-card-foreground">Traffic sources</p>
      <p className="mb-5 text-sm text-muted-foreground">Sessions in the last 30 days</p>
      <DonutChart data={traffic} totalLabel="Sessions" />
    </div>
  );
}
```

## AI prompt

```text
Add the "Donut Chart Segments" component from PixelPapa UI to this project.

What it is: A donut whose segments sweep in one after another; hovering a segment or its legend row thickens it and swaps the centre total for that slice.
Category: Charts. Depends on: motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/chart-donut-segments.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/chart-donut-segments.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/chart-donut-segments" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More charts

- [Area Chart Gradient](https://pixelpapa.com/components/charts/chart-area-gradient.md) (Pro): A smooth SVG area chart that draws its line on mount, fades in a gradient fill and snaps a crosshair tooltip to the nearest point.
- [Funnel Steps Chart](https://pixelpapa.com/components/charts/chart-funnel-steps.md) (Pro): A flowing horizontal funnel: each stage tapers into the next with a bezier, stages fade in left to right and hovering one shows its drop-off.
- [Grouped Bar Chart](https://pixelpapa.com/components/charts/chart-bar-grouped.md) (Pro): Side-by-side SVG bars per category that grow from the baseline in a stagger, with a group highlight and a tooltip comparing every series.
- [Heat Grid Week by Hour](https://pixelpapa.com/components/charts/chart-heat-grid.md) (Pro): A 7×24 SVG heatmap whose cells ripple in diagonally; hover or arrow-key between cells for a snapped tooltip, with peak hour called out.
- [Inline Sparkline](https://pixelpapa.com/components/charts/chart-sparkline-inline.md): A tiny table-cell sparkline that draws itself on mount, colours by trend and snaps a value bubble to the nearest point on hover.
- [Multi Line Chart With Legend](https://pixelpapa.com/components/charts/chart-line-multi-legend.md) (Pro): Several SVG lines draw in together; legend buttons toggle a series and the y-axis rescales while the remaining lines morph to fit.
