Ring Chart

A composable multi-ring progress chart with animated arcs, hover interactions, and a reusable legend component

Preview

12,847Total Sessions

Sessions by Channel

Organic4,25085%
Paid3,12062%
Email2,10042%
Social1,58032%
Referral1,05021%
Direct74715%

Installation

pnpm dlx shadcn@latest add https://ui.bklit.com/r/ring-chart.json

Usage

The Ring Chart uses a composable API similar to other charts in the library. Build charts by combining components:

import { RingChart, Ring, RingCenter } from "@bklitui/ui/charts";

const data = [
  { label: "Organic", value: 4250, maxValue: 5000, color: "#0ea5e9" },
  { label: "Paid", value: 3120, maxValue: 5000, color: "#a855f7" },
  { label: "Email", value: 2100, maxValue: 5000, color: "#f59e0b" },
];

export default function SessionsChart() {
  return (
    <RingChart data={data} size={300}>
      {data.map((item, index) => (
        <Ring key={item.label} index={index} />
      ))}
      <RingCenter defaultLabel="Total Sessions" />
    </RingChart>
  );
}

Components

RingChart

The root component that provides context to all children.

PropTypeDefaultDescription
dataRingData[]requiredArray of ring data items
sizenumberautoFixed size in pixels (uses parent if not set)
strokeWidthnumber12Width of each ring
ringGapnumber6Gap between rings
baseInnerRadiusnumber60Inner radius of the innermost ring
hoveredIndexnumber | null-Controlled hover state
onHoverChange(index: number | null) => void-Hover state callback
classNamestring""Additional CSS class

Ring

Renders an individual ring with background track and animated progress arc.

PropTypeDefaultDescription
indexnumberrequiredIndex of the ring in the data array
colorstringfrom data/paletteOptional color override
animatebooleantrueEnable animation on mount
showGlowbooleantrueShow glow effect on hover
lineCap"round" | "butt""round"Line cap style for ring ends

RingCenter

Displays the total or hovered value in the center of the chart.

PropTypeDefaultDescription
defaultLabelstring"Total"Label shown when not hovering
formatValue(value: number) => stringtoLocaleString()Format function for values
childrenfunction-Custom render function
classNamestring""Additional CSS class

Legend

A composable legend component for ring charts, pie charts, and other visualizations. See the full Legend documentation for all components and options.

Data Shape

interface RingData {
  label: string;      // Display label
  value: number;      // Current value
  maxValue: number;   // Maximum value (for percentage)
  color?: string;     // Optional color (falls back to palette)
}

interface LegendItem {
  label: string;
  value: number;
  maxValue?: number;  // Required if showProgress is true
  color: string;
}

Examples

Basic Ring Chart

12,847Total
<RingChart data={sessionsData} size={280}>{sessionsData.map((_, index) => (  <Ring key={index} index={index} />))}<RingCenter /></RingChart>

With Custom Colors and Formatting

$170KTotal
const customData = [{ label: "Revenue", value: 85000, maxValue: 100000, color: "var(--chart-1)" },{ label: "Expenses", value: 62000, maxValue: 100000, color: "var(--chart-2)" },{ label: "Profit", value: 23000, maxValue: 100000, color: "var(--chart-3)" },];<RingChart data={customData} size={240} strokeWidth={16} ringGap={8}>{customData.map((_, index) => (  <Ring key={index} index={index} />))}<RingCenter  formatValue={(v) => `$${(v / 1000).toFixed(0)}k`}  defaultLabel="Total"/></RingChart>

Synchronized Legend

Connect the legend hover state to the chart for bidirectional interaction. See the Legend documentation for more customization options.

import { useState } from "react";
import { 
  RingChart, Ring, RingCenter,
  Legend, LegendItemComponent, LegendMarker, LegendLabel, LegendValue, LegendProgress
} from "@bklitui/ui/charts";

function SyncedRingChart() {
  const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);

  return (
    <div className="flex items-center gap-12">
      <RingChart
        data={data}
        size={320}
        hoveredIndex={hoveredIndex}
        onHoverChange={setHoveredIndex}
      >
        {data.map((item, index) => (
          <Ring key={item.label} index={index} />
        ))}
        <RingCenter />
      </RingChart>

      <Legend
        items={data}
        hoveredIndex={hoveredIndex}
        onHoverChange={setHoveredIndex}
        title="Sessions by Channel"
      >
        <LegendItemComponent className="grid grid-cols-[auto_1fr_auto] items-center gap-x-3 gap-y-1">
          <LegendMarker />
          <LegendLabel />
          <LegendValue showPercentage />
          <div className="col-span-full">
            <LegendProgress />
          </div>
        </LegendItemComponent>
      </Legend>
    </div>
  );
}

Custom Center Content

Use the render prop for complete control over the center content:

<RingChart data={data} size={300}>
  {data.map((_, index) => (
    <Ring key={index} index={index} />
  ))}
  <RingCenter>
    {({ value, label, isHovered, data }) => (
      <div className="text-center">
        <div className="text-3xl font-bold" style={{ color: data.color }}>
          {value.toLocaleString()}
        </div>
        <div className="text-sm text-muted-foreground">{label}</div>
        {isHovered && (
          <div className="text-xs text-muted-foreground mt-1">
            {((data.value / data.maxValue) * 100).toFixed(0)}% of goal
          </div>
        )}
      </div>
    )}
  </RingCenter>
</RingChart>

Simple Legend (No Progress Bars)

<Legend items={data} title="Traffic Sources">
  <LegendItemComponent className="flex items-center gap-3">
    <LegendMarker />
    <LegendLabel className="flex-1" />
    <LegendValue />
  </LegendItemComponent>
</Legend>

Flat Ring Ends (Butt Line Cap)

Use lineCap="butt" for square/flat ring ends instead of rounded:

<RingChart data={data} size={300}>
  {data.map((item, index) => (
    <Ring key={item.label} index={index} lineCap="butt" />
  ))}
  <RingCenter />
</RingChart>

Theming

The Ring Chart uses CSS variables for theming. The ring background uses --chart-ring-background, and ring colors default to --chart-1 through --chart-5:

:root {
  --chart-ring-background: oklch(0.9 0.005 260 / 0.25);
  --chart-1: oklch(0.646 0.222 41.116);
  --chart-2: oklch(0.6 0.118 184.704);
  --chart-3: oklch(0.398 0.07 227.392);
  --chart-4: oklch(0.828 0.189 84.429);
  --chart-5: oklch(0.769 0.188 70.08);
}

.dark {
  --chart-ring-background: oklch(0.35 0.01 260 / 0.25);
  --chart-1: oklch(0.488 0.243 264.376);
  --chart-2: oklch(0.696 0.17 162.48);
  --chart-3: oklch(0.769 0.188 70.08);
  --chart-4: oklch(0.627 0.265 303.9);
  --chart-5: oklch(0.645 0.246 16.439);
}

Animation

The ring chart features a multi-phase animation on mount:

  1. Ring Expansion - Background rings scale in with staggered timing
  2. Progress Arcs - Progress arcs animate from 0 to their target value
  3. Center Content - Value and label fade in
  4. Legend - Items slide in from the right with progress bars filling

All animations use spring physics for natural motion.

Dependencies

pnpm add @visx/shape @visx/group @visx/responsive motion