Experience System
Components

Circular Progress

Circular track and indicator for determinate or indeterminate progress, with optional centered label or value text.

66%

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-system

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

66%

Sizes

Use size to scale the ring: md (default), lg, or xl.

25%
55%
80%

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.

Determinate
72%
Indeterminate
Syncing...

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

PropTypeDefault
sizemd | lg | xlmd
valuenumber | nullnull (indeterminate when omitted or null)
maxnumber100 (values ≤ 0 fall back to 100)
getValueLabel(value: number, max: number) => stringundefined
classNamestringundefined
Data attributeValues
data-slotcircular-progress
data-statedeterminate | indeterminate
data-valueCurrent numeric value (determinate only)
data-maxMaximum 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.

PropTypeDefault
classNamestringundefined
Data attributeValues
data-slotcircular-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.

PropTypeDefault
classNamestringundefined
Data attributeValues
data-slotcircular-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.