ProgressCircle
A ring that shows how far a task has gone, sized for cards, tiles and toolbars. When there is room for a full-width bar, use Progress.
Installation
Set up Camaleo once (Installation), then import what you need. It renders on the server.
import { ProgressCircle } from "@camaleo/react";Anatomy
A root with the progressbar role holds the ring (an SVG with a track and an arc, plus the value on top) and an optional label below. Every part has a data-slot you can target from CSS.
Examples
Every example is a real file you can copy. They all wear the skin, mode and brand from the bar above.
Stuck or failed
Use tone="warning" when the task is stuck and tone="danger" when it failed. Say why in the label too, so the color is not the only signal.
Sizes
Change size and strokeWidth together. A thin stroke on a big ring looks faint, and a thick one on a small ring crowds the number.
Unknown duration
Pass value={null} when you can't measure the work yet: the number hides and the arc spins (it pulses with reduced motion). Leaving value out is not the same, it means 0%.
Count instead of percent
Set max to the real total and show "3/5" in valueLabel. getAriaValueText gives screen readers the words, "3 of 5 lessons", instead of "60%".
Tiny ring next to text
Below about 40 px the number doesn't fit. Hide it with showValue={false}, write the value beside the ring and name the ring with aria-label.
A measurement, not a task
Storage used never moves toward "done". Pass role="meter" so screen readers don't announce it as a task in progress.
Announce when it's done
Click until it finishes. Screen readers don't announce every change of the ring, so a role="status" message says when all the photos are in.
In context
Rings fit where a bar would be too wide: next to a title, in a card or in a row of goals. These are whole pieces of UI you can copy, in the skin you picked.
Uploading to Shared drive
4 of 6 files · about 1 minute left
Today's activity
Wednesday, October 28, 2026
TypeScript for React developers
Next lesson: Generic components
Guidelines
"Importing contacts" tells people what they are waiting for, and becomes the ring's accessible name.
40% of what? Without a label, people can't tell which task it tracks, and screen readers only hear "Progress".
A red ring next to "Upload failed" points people to the one thing that needs them.
A profile setup at 25% is normal. Red there teaches people to ignore red when something really breaks.
Props
min and max (clamped). null means the duration is unknown and the ring spins; leaving it out means 0.warning when the task is stuck, danger when it failed.aria-label is set. Without either, the name is "Progress" (or the MessagesProvider text).value is null.getAriaValueText.aria-valuetext). Gets the shown percentage (like "40%") and the
value, both null while indeterminate.Native <div> attributes pass through, like aria-label, aria-valuetext and role.
Styling hooks
Restyle it without forking: target the attributes, or set the tokens on a parent, a skin or the ring itself.
Attributes
Tokens it reads
Color
Type & motion
Accessibility
- It renders role="progressbar" with aria-valuenow, aria-valuemin and aria-valuemax. The label names it (any content, not only a string); without label or aria-label, the name is just "Progress", from the MessagesProvider.
- Screen readers hear the percentage, or valueLabel when it is a string. When it isn't, or when the visible text is short like "3/5", pass getAriaValueText so screen readers hear the whole phrase.
- With value={null}, aria-valuenow is left out, which tells assistive tech the duration is unknown.
- Changes are not announced. When the task ends or fails, say so in a status message (role="status").