Base components
Progress circle
Progress shown as a ring or a half ring with the value in the middle, where the arc sweeps to each new value and the number rolls along with it.
Live preview
Loading preview
The five sizes as rings at 40%, the same as half rings, and a storage card where the ring goes from 40% to 72% with the caption "Storage used". Replay sweeps every arc again from 0.
Installation
Run this in your app after the one-time setup on the Getting started page.
$ npx @ganisigit/lunia-ds add progress-circleUsage
The demo above, as code.
demo.tsx
'use client';
import * as React from 'react';
import { ProgressCircle } from './progress-circle';
import type { ProgressCircleSize } from './progress-circle-variants';
const SIZES: ProgressCircleSize[] = ['xxs', 'xs', 'sm', 'md', 'lg'];
export default function ProgressCircleDemo({ replay = 0 }: { replay?: number }) {
const [storage, setStorage] = React.useState(72);
React.useEffect(() => {
if (replay === 0) return;
setStorage(40);
const timer = window.setTimeout(() => setStorage(72), 1200);
return () => window.clearTimeout(timer);
}, [replay]);
return (
// The key remounts the circles on Replay, so every arc sweeps again from 0.
<div key={replay} className="flex flex-col items-center gap-10">
<div className="flex flex-wrap items-center justify-center gap-6">
{SIZES.map((size) => (
<ProgressCircle
key={size}
size={size}
value={40}
caption={size === 'xxs' ? 'Users' : 'Active users'}
aria-label="Active users"
/>
))}
</div>
<div className="flex flex-wrap items-end justify-center gap-6">
{SIZES.map((size) => (
<ProgressCircle
key={size}
size={size}
shape="half-circle"
value={40}
caption={size === 'xxs' ? 'Users' : 'Active users'}
aria-label="Active users"
/>
))}
</div>
<div className="flex items-center gap-5 rounded-xl border border-secondary bg-primary p-5">
<ProgressCircle size="xxs" value={storage} aria-labelledby="storage-title" />
<div className="flex flex-col">
<span id="storage-title" className="text-sm font-semibold text-primary">
Storage used
</span>
<span className="text-sm text-tertiary">7.2 GB of 10 GB</span>
</div>
</div>
</div>
);
}
API
Parts: root, track (full ring or top half), arc (round caps), value label (optional caption above the number), under the ring at xxs.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | number | 0 | Progress from min to max. |
| min | number | 0 | Lowest value. |
| max | number | 100 | Highest value. |
| size | "xxs" | "xs" | "sm" | "md" | "lg" | "sm" | 64, 160, 200, 240 or 280px. xxs puts the label under the ring. |
| shape | "circle" | "half-circle" | "circle" | Full ring, or an arc over the top that fills from left to right. |
| showLabel | boolean | true | Shows the value in the middle (or under the ring at xxs). |
| caption | string | undefined | Small line above the number (Figma), for example "Active users". |
| format | (value: number, max: number) => string | percent, for example "40%" | Text of the value label and of aria-valuetext. |
| animateOnMount | boolean | true | Sweeps the arc from 0 to the first value when the circle first shows. |
| Slot | Description |
|---|---|
| label | Custom content in the middle, replaces the number and caption. |
Motion
Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.
| ID | Trigger | Element | Property | From → to | Timing | Reduced motion |
|---|---|---|---|---|---|---|
| PCR1 | value changes | arc | stroke-dashoffset (the arc length) | old value → new value | duration-5xl (640)ease-out | Resizes at once |
| PCR2 | value changes | value label number | number roll (number-roll) | old percent → new percent | duration-5xl (640)ease-out | Swaps at once |
| PCR3 | first time the circle is in view (animateOnMount) | arc, label | sweep and number roll from 0 (as PCR1 and PCR2) | 0 → value | duration-5xl (640)ease-out | Shows at once |
Accessibility
- Built on a progress primitive: role progressbar with aria-valuenow, aria-valuemin, aria-valuemax and aria-valuetext from format.
- Needs a name: aria-label or aria-labelledby, for example the caption.
- The SVG is aria-hidden; the rolling number is aria-hidden during the roll and screen readers read aria-valuetext.
- Reduced motion: the arc and the number jump to the new value.