# 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"`
- **Radix Progress:** <https://www.radix-ui.com/primitives/docs/components/progress>
- **Page:** <https://ui.booleanpress.com/components/progress-circle> · @booleanpress/ui 0.2.0

## 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.

```tsx
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`.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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

| Key | Behaviour |
| --- | --- |

## API

### ProgressCircle

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `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. |
| `max` | `number` |  | The top of the range. 100 by default, and 100 when it is not a positive number. |
| `showValue` | `boolean` | `false` | Writes the value in the middle: `getValueLabel`'s text, a percentage by default. Not drawn at `sm`. |
| `size` | `"default" \| "sm" \| "lg"` | `default` | The ring's size: `sm` 24 px, `default` 40 px, `lg` 64 px. |
| `strokeWidth` | `number` |  | The ring's thickness in pixels at its own size. 3, 4 and 6 px for `sm`, `default` and `lg`. |
| `value` | `number \| null` |  | The 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"` | `default` | The 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).

| Token | Used for |
| --- | --- |
| `--border` | stroke |
| `--destructive` | text |
| `--foreground` | text |
| `--info` | text |
| `--primary` | text |
| `--success` | text |
| `--warning` | text |
