Circular Progress
Circular track and indicator for determinate or indeterminate progress, with optional centered label or value text.
CircularProgress renders an SVG ring with a track and indicator. Pass value and max for determinate progress; omit value (or pass null) for indeterminate animation. Use CircularProgressLabel and CircularProgressValueText for centered content, or provide your own children.
Installation
The component is exported from @by/experience-system. Add the package with your package manager:
pnpm add @by/experience-systemIn this monorepo, depend on the workspace package (for example via workspace:* or your catalog) so imports resolve to packages/experience-system.
Composition
Use the following composition to build a CircularProgress:
CircularProgress
├── CircularProgressLabel (optional)
│ └── CircularProgressValueText (optional)
└── (custom children, optional — centered in the ring)CircularProgress is the root (role="progressbar"). CircularProgressLabel and CircularProgressValueText are optional helpers for typography and layout inside the ring; you can pass other children instead. The SVG track and indicator are internal. This component is not a Radix primitive—it follows HTML progress bar semantics on a styled div.
Usage
import {
CircularProgress,
CircularProgressLabel,
CircularProgressValueText,
} from '@by/experience-system';CircularProgress is a client component ('use client' in the package). Use it inside a Client Component or a dynamic import when using the Next.js App Router.
Determinate progress with an automatic percentage in the center (CircularProgressValueText defaults to the rounded percentage when children are omitted):
<CircularProgress value={72} max={100} size="md" aria-labelledby="upload-label">
<CircularProgressLabel>
<CircularProgressValueText />
</CircularProgressLabel>
</CircularProgress>Use getValueLabel on CircularProgress when screen readers need custom aria-valuetext (it also affects the default accessible value string when CircularProgressValueText renders the numeric percentage).
Indeterminate mode
Omit value or pass null for an indeterminate ring. For generic or in-button loading, prefer Spinner—it behaves like an inline icon and stays compact. Use indeterminate CircularProgress when a circular ring is deliberately part of the layout, not as a substitute for Spinner.
Spinner vs Circular Progress
Spinner is the recommended default for action loading, buttons (including the outline loading pattern), and other generic indeterminate states: it is a lighter, simpler rotation and fits dense UI. CircularProgress is aimed at progress visualization—especially determinate completion—with indeterminate mode reserved for when that circular affordance is specifically desired. The SVG implementation is heavier than Spinner; avoid treating the two as interchangeable for the same loading-indicator purpose.
Examples
Overview
Pair CircularProgress with Field and FieldLabel when the ring sits next to a field label, or provide aria-labelledby / aria-label so the bar has an accessible name.
Sizes
Use size to scale the ring: md (default), lg, or xl.
Determinate and indeterminate
Determinate mode sets aria-valuenow, aria-valuemin, and aria-valuemax. Indeterminate mode omits those and spins the SVG; provide aria-label (or a labelled-by relationship) when there is no visible label.
API Reference
CircularProgress is a Experience System component (not a Radix primitive). It forwards native HTML div attributes in addition to the props below. Subparts forward HTML span attributes.
CircularProgress
| Prop | Type | Default |
|---|---|---|
size | md | lg | xl | md |
value | number | null | null (indeterminate when omitted or null) |
max | number | 100 (values ≤ 0 fall back to 100) |
getValueLabel | (value: number, max: number) => string | undefined |
className | string | undefined |
| Data attribute | Values |
|---|---|
data-slot | circular-progress |
data-state | determinate | indeterminate |
data-value | Current numeric value (determinate only) |
data-max | Maximum value |
Internal SVG parts use data-slot values circular-progress-svg, circular-progress-track, and circular-progress-indicator. Centered content is wrapped with data-slot="circular-progress-content" when children are present.
CircularProgressLabel
Applies label typography for content inside the ring. Requires a CircularProgress ancestor.
| Prop | Type | Default |
|---|---|---|
className | string | undefined |
| Data attribute | Values |
|---|---|
data-slot | circular-progress-label |
CircularProgressValueText
Renders centered value text; if children are omitted in determinate mode, shows the rounded percentage. Renders nothing when indeterminate and children are omitted. Requires a CircularProgress ancestor.
| Prop | Type | Default |
|---|---|---|
className | string | undefined |
| Data attribute | Values |
|---|---|
data-slot | circular-progress-value-text |
Accessibility
The root uses role="progressbar". In determinate mode, aria-valuenow, aria-valuemin, aria-valuemax, and aria-valuetext reflect value, max, and getValueLabel. In indeterminate mode, value attributes are omitted and the SVG is aria-hidden; provide a visible label or aria-label / aria-labelledby so the purpose is clear. Reduced motion is respected via motion-reduce on animations.
Source in the repo: packages/experience-system/src/components/CircularProgress/CircularProgress.tsx and variants.ts. Agent-oriented contracts: packages/experience-system/src/components/CircularProgress/CircularProgress.instructions.md.