Ring Chart
A composable multi-ring progress chart with animated arcs, hover interactions, and a reusable legend component
Preview
Sessions by Channel
Installation
pnpm dlx shadcn@latest add https://ui.bklit.com/r/ring-chart.jsonUsage
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.
| Prop | Type | Default | Description |
|---|---|---|---|
data | RingData[] | required | Array of ring data items |
size | number | auto | Fixed size in pixels (uses parent if not set) |
strokeWidth | number | 12 | Width of each ring |
ringGap | number | 6 | Gap between rings |
baseInnerRadius | number | 60 | Inner radius of the innermost ring |
hoveredIndex | number | null | - | Controlled hover state |
onHoverChange | (index: number | null) => void | - | Hover state callback |
className | string | "" | Additional CSS class |
Ring
Renders an individual ring with background track and animated progress arc.
| Prop | Type | Default | Description |
|---|---|---|---|
index | number | required | Index of the ring in the data array |
color | string | from data/palette | Optional color override |
animate | boolean | true | Enable animation on mount |
showGlow | boolean | true | Show 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.
| Prop | Type | Default | Description |
|---|---|---|---|
defaultLabel | string | "Total" | Label shown when not hovering |
formatValue | (value: number) => string | toLocaleString() | Format function for values |
children | function | - | Custom render function |
className | string | "" | 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
<RingChart data={sessionsData} size={280}>{sessionsData.map((_, index) => ( <Ring key={index} index={index} />))}<RingCenter /></RingChart>With Custom Colors and Formatting
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:
- Ring Expansion - Background rings scale in with staggered timing
- Progress Arcs - Progress arcs animate from 0 to their target value
- Center Content - Value and label fade in
- 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