# Circular Progress (/docs/circular-progress)





`CircularProgress` is the ring counterpart of [`Progress`](/docs/progress) — same Ark UI machine (so `role="progressbar"`, the `aria-value*` wiring, and indeterminate come for free), same variant × color axes. The arc is a stroked SVG circle, so it clips cleanly at any size.

```tsx
import { CircularProgress } from '@42-central/ui-react/circular-progress';

<CircularProgress value={65} />
<CircularProgress variant="gradient" value={70} />
<CircularProgress value={80}>80%</CircularProgress>
<CircularProgress value={null} /> {/* indeterminate */}
```

## Playground [#playground]

<story.Playground />

### Full prop browser [#full-prop-browser]

<story.WithControl />

## Variants [#variants]

Two arc treatments. `filled` (default) routes through the palette; `gradient` paints its own stops.

<Preview align="center" code="&#x22;<CircularProgress variant=\&#x22;filled\&#x22; value={65} />\n<CircularProgress variant=\&#x22;gradient\&#x22; value={65} />&#x22;">
  <div className="flex flex-wrap items-center gap-6">
    <CircularProgress variant="filled" value="65" size="lg" />

    <CircularProgress variant="gradient" value="65" size="lg" />
  </div>
</Preview>

## Colors [#colors]

Any palette color pairs with the `filled` variant via the kit's `data-color` system.

<Preview align="center" code="&#x22;<CircularProgress color=\&#x22;green\&#x22; value={70} />\n<CircularProgress color=\&#x22;orange\&#x22; value={45} />\n<CircularProgress color=\&#x22;red\&#x22; value={20} />&#x22;">
  <div className="flex flex-wrap items-center justify-center gap-4">
    {COLORS.map((c) => (
          <CircularProgress key={c} color={c} value={66} size="md" />
        ))}
  </div>
</Preview>

## Gradient [#gradient]

Like the linear [`Progress`](/docs/progress#gradient) bar, the gradient is fixed to the **whole ring** and the arc reveals it — so the complete `from`→`to` only shows once the ring closes at 100%. Pass a `gradient` config (`from` / `to` / `via`) to customize the stops (`dir` is ignored — the SVG sweep runs top-left → bottom-right).

<Preview align="center" code="&#x22;<CircularProgress variant=\&#x22;gradient\&#x22; value={40} />\n<CircularProgress variant=\&#x22;gradient\&#x22; value={70} />\n<CircularProgress variant=\&#x22;gradient\&#x22; value={100} />&#x22;">
  <div className="flex flex-wrap items-center gap-6">
    <CircularProgress variant="gradient" value="40" size="lg" />

    <CircularProgress variant="gradient" value="70" size="lg" />

    <CircularProgress variant="gradient" value="100" size="lg" />

    <CircularProgress variant="gradient" value="70" size="lg" gradient="{ from: 'var(--color-cyan-400)', to: 'var(--color-blue-600)' }" />
  </div>
</Preview>

## Sizes [#sizes]

Five diameters, matching the kit's `xs`–`xl` scale. Override the stroke width with `thickness`.

<Preview align="center" code="&#x22;<CircularProgress size=\&#x22;xs\&#x22; value={65} />\n<CircularProgress size=\&#x22;sm\&#x22; value={65} />\n<CircularProgress size=\&#x22;md\&#x22; value={65} />\n<CircularProgress size=\&#x22;lg\&#x22; value={65} />\n<CircularProgress size=\&#x22;xl\&#x22; value={65} />&#x22;">
  <div className="flex flex-wrap items-center gap-4">
    <CircularProgress size="xs" value="65" />

    <CircularProgress size="sm" value="65" />

    <CircularProgress size="md" value="65" />

    <CircularProgress size="lg" value="65" />

    <CircularProgress size="xl" value="65" />
  </div>
</Preview>

Both `size` and `thickness` also accept a raw px number, independently of each other — handy for a diameter or stroke weight that doesn't fit the tee-shirt scale.

<Preview align="center" code="`<CircularProgress size={100} thickness={8} value={40} />`">
  <CircularProgress size="100" thickness="8" value="40" />
</Preview>

## Label [#label]

Pass `children` to render centered content — typically a percentage readout.

<Preview align="center" code="&#x22;<CircularProgress value={80} size=\&#x22;xl\&#x22; color=\&#x22;green\&#x22;>\n  <span className=\&#x22;text-sm font-mono font-medium\&#x22;>80%</span>\n</CircularProgress>&#x22;">
  <CircularProgress value="80" size="xl" color="green">
    <span className="text-sm font-mono font-medium">
      80%
    </span>
  </CircularProgress>
</Preview>

## Indeterminate [#indeterminate]

When you can't measure progress, pass `value={null}` — a fixed arc spins (disabled under the OS "reduce motion" setting, where it rests in place). Toggling indeterminate on or off morphs the arc between the value and the spinner rather than snapping — toggle it in the [playground](#playground) to see it.

<Preview align="center" code="`<CircularProgress value={null} />`">
  <div className="flex flex-wrap items-center gap-6">
    <CircularProgress value="null" size="lg" />

    <CircularProgress value="null" size="lg" color="green" />

    <CircularProgress value="null" size="lg" variant="gradient" />
  </div>
</Preview>

## API [#api]

<TypeTable
  type="{
  variant: {
    type: &#x22;'filled' | 'gradient'&#x22;,
    default: &#x22;'filled'&#x22;,
    description: 'Arc treatment. `filled` use the palette; `gradient` paints its own stops.',
  },
  value: {
    type: 'number | null',
    description: 'Controlled value in min..max. Pass `null` for the indeterminate state.',
  },
  defaultValue: {
    type: 'number | null',
    description: 'Uncontrolled initial value.',
  },
  onChange: {
    type: '(value: number | null) => void',
    description: 'Fires on every value change.',
  },
  min: { type: 'number', default: '0', description: 'Lower bound of the value space.' },
  max: { type: 'number', default: '100', description: 'Upper bound of the value space.' },
  color: {
    type: &#x22;'brand' | 'neutral' | 'red' | 'orange' | 'yellow' | 'green' | 'teal' | 'cyan' | 'blue' | 'purple' | 'pink' | (string & {})&#x22;,
    default: &#x22;'brand'&#x22;,
    description: 'Accent palette. Ignored when variant is `gradient`.',
  },
  gradient: {
    type: '{ from?: string; to?: string; via?: string }',
    description: 'Gradient stops. Only applied when variant is `gradient` (`dir` is ignored).',
  },
  size: {
    type: &#x22;'xs' | 'sm' | 'md' | 'lg' | 'xl' | number&#x22;,
    default: &#x22;'md'&#x22;,
    description: 'Ring diameter. Pass a tee-shirt size, or a raw number for a custom px diameter.',
  },
  thickness: {
    type: &#x22;'xs' | 'sm' | 'md' | 'lg' | 'xl' | number&#x22;,
    description: 'Stroke width in px, or a tee-shirt size (looked up independently of `size`). Defaults to a per-size value.',
  },
  children: { type: 'ReactNode', description: 'Centered content, e.g. a percentage label.' },
  testId: {
    type: 'string',
    description: 'Renders as `data-testid` on the ring — the actual `role=&#x22;progressbar&#x22;` node — not the outer root wrapper. See [Testing](/docs/testing).',
  },
}"
/>

All other props are forwarded to the underlying Ark `Progress.Root`.

## Accessibility [#accessibility]

Ark stamps `role="progressbar"` plus `aria-valuenow` / `aria-valuemin` / `aria-valuemax` on the ring. In the indeterminate state `aria-valuenow` is dropped. Provide an `aria-label` so the control names what it tracks:

```tsx
<CircularProgress value={uploaded} aria-label="Upload progress" />
```
