CircleProgress is a compact ring that visualises value out of total as a
clockwise arc. When value >= total the arc fades out and a checkmark fades
in on top of a filled ring. Both transitions are 200–300ms, so the component
is at home inline with status text.
Reach for CircleProgress when the reader benefits from seeing a proportion
at a glance: three of five onboarding steps done, forty of a hundred records
imported. For an open-ended wait where no percentage is available, use
<Loading> instead.
Import
import { CircleProgress } from "@unkey/ui";
Progress
The arc grows clockwise from twelve o’clock as value approaches total.
Any ratio above 1 is clamped to 100%, at which point the checkmark state
takes over.
Variants
variant recolors the progress arc, the completed ring, and the checkmark
together. primary is the neutral default; success, warning, and error
signal semantic intent; secondary sits quietly next to body copy.
Size
iconSize uses the shared size-weight system from @unkey/icons, so the
ring aligns with neighboring icons and text. Size picks the outer diameter
(sm, md, lg, xl, 2xl); weight picks the stroke thickness (thin,
medium, regular, bold).
Stroke weight scales independently of diameter. Pair a larger ring with a thinner stroke when the component sits in a hero empty state, or a smaller ring with a bolder stroke to keep it legible inside a table row.
Completion
When value reaches or exceeds total, the arc fades out and a filled
ring with a checkmark fades in. The swap happens automatically — you don’t
need to conditionally render a separate “done” state.
Accessibility
CircleProgress renders a decorative <svg> inside a plain <div> with no
built-in ARIA semantics. When the ring represents a real in-flight
operation, add the semantics on the wrapping element yourself:
- Set
role="progressbar"and the matchingaria-valuenow,aria-valuemin, andaria-valuemaxon the wrapping<div>(via the pass-through props), or on a sibling container. - Add an
aria-labeloraria-labelledbythat names what is progressing:"Onboarding steps","Import progress". A generic “progress” is rarely enough. - Once
value >= total, announce completion through the surrounding region (for example with anaria-live="polite"status message). The checkmark swap is purely visual.
If the ring is decorative (say, a placeholder glyph), leave it unlabeled and
wrap it in a container with aria-hidden="true".
Props
value number Required Current progress, expressed in the same units as total. Must be
non-negative and finite; the component throws if it isn’t.
total number Required The value that represents 100% completion. Must be greater than zero and finite; the component throws otherwise. Ratios above 1 are clamped.
variant "primary" | "secondary" | "success" | "warning" | "error" Optional
Default "primary" Semantic color applied to the progress arc, the completed ring, and the checkmark. Pick the intent that matches the state, not the hue.
iconSize "sm-thin" | "sm-medium" | ... | "2xl-bold" Optional
Default "md-regular" Size token from the shared @unkey/icons sizing system. The first half
(sm through 2xl) picks the outer diameter (12px through 30px); the
second half (thin, medium, regular, bold) picks the stroke
thickness.
className string Optional Additional Tailwind classes. Merged with the variant classes via cn.
...rest HTMLAttributes<HTMLDivElement> Optional All standard <div> attributes pass through to the wrapping element, so
this is where you attach role="progressbar", aria-valuenow, and
friends.
Related
- Loading — for open-ended waits where you can’t express a percentage.
- Button —
<Button loading>embeds its own spinner when the action is tied to a button press.