Skip to the content

ComponentsMisc

Progress circle

A ring that fills to show how far a task has got, or turns while work of unknown length runs, where a bar has no room.

Import

import { ProgressCircle } from "@booleanpress/ui/progress-circle"

Usage

Pass value, a number from 0 to 100, and a name. The name is required: aria-label, or aria-labelledby pointing at visible text.

import { ProgressCircle } from "@booleanpress/ui/progress-circle"

export function StorageUsed() {
  return <ProgressCircle value={75} showValue aria-label="Storage used" />
}

It is controlled only, like Progress: it shows the value you pass. The ring starts at the top and fills clockwise (counter-clockwise on a right-to-left page). max changes the top of the range.

No value makes it indeterminate: a quarter of the ring turns, and aria-valuenow is left out. For a busy state inside a button or a field, the smaller Spinner is the better fit.

The value inside. showValue writes the value in the middle, as a percentage in the provider's locale; getValueLabel replaces the text, and is the ring's aria-valuetext too. It is not drawn at sm.

Sizes and colours. size is sm (24 px), default (40 px) or lg (64 px), with a 3, 4 or 6 px ring; strokeWidth sets another thickness. variant colours the filled arc: default (the primary colour), success, info, warning or destructive. Say what the colour means in text as well.

Examples

Basic

A ring at 75 %, named with aria-label.

import { ProgressCircle } from "@booleanpress/ui/progress-circle"

export default function ProgressCircleBasic() {
  return <ProgressCircle value={75} aria-label="Storage used" />
}

Indeterminate

No value: a quarter of the ring turns, named by the text beside it.

import { ProgressCircle } from "@booleanpress/ui/progress-circle"

export default function ProgressCircleIndeterminate() {
  return (
    <div className="flex items-center gap-3">
      <ProgressCircle size="sm" aria-labelledby="checking-dns" />
      <span id="checking-dns" className="text-sm">
        Checking the DNS records…
      </span>
    </div>
  )
}

Sizes

size sm, default and lg: 24, 40 and 64 px.

import { ProgressCircle } from "@booleanpress/ui/progress-circle"

export default function ProgressCircleSizes() {
  return (
    <div className="flex items-center gap-6">
      <ProgressCircle size="sm" value={40} aria-label="Small, 40 percent" />
      <ProgressCircle value={60} aria-label="Default, 60 percent" />
      <ProgressCircle size="lg" value={80} aria-label="Large, 80 percent" />
    </div>
  )
}

With a label

showValue writes the percentage in the middle; the large ring is named by the text beside it.

import { ProgressCircle } from "@booleanpress/ui/progress-circle"

export default function ProgressCircleWithLabel() {
  return (
    <div className="flex items-center gap-6">
      <ProgressCircle value={75} showValue aria-label="Monthly sending quota" />
      <div className="flex items-center gap-3">
        <ProgressCircle size="lg" value={42} showValue aria-labelledby="quota-label" />
        <div className="text-sm">
          <p id="quota-label" className="font-medium">
            Monthly sending quota
          </p>
          <p className="text-muted-foreground">4,200 of 10,000 emails</p>
        </div>
      </div>
    </div>
  )
}

Colours

variant success, info, warning and destructive on the status colours, each named for what it counts.

import { ProgressCircle } from "@booleanpress/ui/progress-circle"

export default function ProgressCircleColours() {
  return (
    <div className="flex items-center gap-6">
      <ProgressCircle size="lg" variant="success" value={98} showValue aria-label="Delivered" />
      <ProgressCircle size="lg" variant="info" value={64} showValue aria-label="Opened" />
      <ProgressCircle size="lg" variant="warning" value={12} showValue aria-label="Deferred" />
      <ProgressCircle size="lg" variant="destructive" value={2} showValue aria-label="Bounced" />
    </div>
  )
}

Accessibility

Semantics
Radix renders a div with role="progressbar", aria-valuemin="0", aria-valuemax, aria-valuenow and aria-valuetext (the percentage in the provider's locale, or getValueLabel's text), plus data-state (indeterminate, loading, complete). With no value, aria-valuenow and aria-valuetext are left out. The ring and the text in it are aria-hidden.
Labels
A name is required, and the types ask for it: aria-label, or aria-labelledby pointing at visible text. Name what is measured ("Monthly sending quota"), not the control ("Progress").
Focus
A progress circle takes no focus and has no keyboard behaviour, so it has no keyboard rows.
Known limits
  • It is not a live region: screen readers do not announce each change. Announce milestones in a live region the page owns.
  • The ring alone is not a precise reading, and its colour carries no meaning by itself: put the number or the state in text when it matters.
  • The turn of the indeterminate ring and the slide to a new value end under prefers-reduced-motion (theme.css): the ring rests with its quarter arc at the top.

Keyboard

Keyboard
KeyBehaviour

API

ProgressCircle

ProgressCircle props
PropTypeDefaultDescription
asChildboolean
getValueLabel((value: number, max: number) => string)Returns the value's text from the value and the max: the ring's aria-valuetext, and the text in the middle with showValue. A percentage in the provider's locale by default.
maxnumberThe top of the range. 100 by default, and 100 when it is not a positive number.
showValuebooleanfalseWrites the value in the middle: getValueLabel's text, a percentage by default. Not drawn at sm.
size"default" | "sm" | "lg"defaultThe ring's size: sm 24 px, default 40 px, lg 64 px.
strokeWidthnumberThe ring's thickness in pixels at its own size. 3, 4 and 6 px for sm, default and lg.
valuenumber | nullThe progress from 0 to max; a value outside the range is clamped to it. Leave it out for the indeterminate ring.
variant"default" | "destructive" | "success" | "info" | "warning"defaultThe colour of the filled arc: the primary colour, or a status colour.

Every part takes className, merged with its defaults by cn(), and ref, which reaches the element it renders.

Data attributes: data-slot="progress-circle" (ProgressCircle), and data-size, data-variant.

Theming

The track is --border; the filled arc is the text colour, --primary by default or --success, --info, --warning and --destructive; the value in the middle is --foreground, 10 px semibold (12 px medium at lg). Set another arc colour with a text-* class, and another size with size-* (the ring scales with it).

The tokens its classes read. Change their values in your theme, and it follows.

Theme tokens
TokenUsed for
--borderstroke
--destructivetext
--foregroundtext
--infotext
--primarytext
--successtext
--warningtext