Skip to content
shadcn.io is not affiliated with official shadcn/ui

Shadcn Circular Progress for React and Tailwind

Shows task progress with a circular indicator.

Installation

bunx --bun shadcn@latest add https://kit.dev/r/circular-progress.json

Anatomy

CircularProgress
├── CircularProgressLabel
├── CircularProgressValue
├── CircularProgressView
└── CircularProgressTrack (baked in)
    ├── CircleTrack
    └── CircleRange

CircularProgress is Ark Progress.Root. CircularProgressTrack is Progress.Circle with track and range baked in. useCircularProgress is the machine hook for CircularProgressRootProvider; useCircularProgressContext / CircularProgressContext is in-tree.

The linear bar is Progress.

Usage

import {
  CircularProgress,
  CircularProgressValue,
} from "@/components/ui/circular-progress";
<CircularProgress value={66} />

The ring is baked in. Add CircularProgressValue (and optionally CircularProgressLabel) as children.

shadcn.io size defaults to 32 and thickness to 4. Those set --size and --thickness on the root and circle. Ark’s defaultValue is 50 when you omit value.

Controlled

Use value and onValueChange. { value } is number | null.

Min and max

Default range is 0100. Set min and max for another scale. 20 with min={10} max={30} is 50%.

Indeterminate

Set indeterminate (shadcn.io) or value={null} (Ark). The range shows a partial arc and the circle spins. Honor reduced motion: spinning is disabled.

Label

CircularProgressLabel provides extra context above the ring.

Root Provider

Use useCircularProgress with CircularProgressRootProvider when you need the API outside the tree. Pass machine options (defaultValue, min, max, formatOptions, …) to useCircularProgress(), not to the provider. Pass size / thickness to the provider (layout, not machine).

Examples

With value

Size

size is the circle width and height in pixels.

Thickness

thickness is the stroke width in pixels.

View

CircularProgressView shows children only when the progress state matches (loading, complete, or indeterminate).

Guides

Styling the circle

Size and thickness are CSS variables on the root and circle. shadcn.io size / thickness set them. You can also set the variables yourself:

[data-slot="circular-progress"] {
  --size: 120px;
  --thickness: 10px;
}
CSS variableDescription
--sizeWidth and height of the circle
--thicknessStroke width of the track and range
--radiuscalc(var(--size) / 2 - var(--thickness) / 2) (set by the machine on the circles)
--circumferencecalc(2 * π * var(--radius))
--offsetDash offset from remaining percent
--percent0–100 percent of the range

Determinate vs indeterminate

Omit indeterminate and pass a number value / defaultValue for a determinate bar. value={null} or indeterminate is the loading state (aria-valuenow omitted).

API Reference

shadcn.io wraps Ark UI Progress (circular parts). The ring is baked into CircularProgress / CircularProgressRootProvider. indeterminate is a shadcn.io convenience for value={null}. size defaults to 32 and thickness to 4.

asChild merges props onto a single child element.

CircularProgress

Root. Renders a div.

PropTypeDefaultDescription
asChildbooleanfalseRender the child element instead of a div.
classNamestring-Class names on the root.
defaultValuenumber | null50Uncontrolled initial value. null is indeterminate.
formatOptionsIntl.NumberFormatOptions{ style: "percent" }How CircularProgressValue formats the value.
idstring-Unique id for the progress machine.
idsPartial<{ root: string; track: string; label: string; circle: string }>-Element ids for composition.
indeterminatebooleanfalseshadcn.io: sets value={null}.
localestring"en-US"Locale for formatting. Usually inherited from LocaleProvider.
maxnumber100Maximum value.
minnumber0Minimum value.
onValueChange(details: ValueChangeDetails) => void-Value changed. { value: number | null }.
orientation"horizontal" | "vertical""horizontal"Exposed as data-orientation. Little visual effect on the circle.
sizenumber32Circle width and height in pixels (--size).
thicknessnumber4Stroke width in pixels (--thickness).
translations{ value?: (details: ValueTranslationDetails) => string }see belowAccessible name / value text. Default indeterminate string is "loading...".
valuenumber | null-Controlled value. null is indeterminate.

Default translations.value uses the formatter (percent by default) or "loading..." when value is null.

AttributeDescription
data-slotcircular-progress
data-scopeprogress
data-partroot
data-maxMaximum value
data-valueCurrent numeric value (omitted when indeterminate)
data-state"loading", "complete", or "indeterminate"
data-orientation"horizontal" or "vertical"
CSS variableDescription
--percentPercent of the range (omitted when indeterminate)
--sizeCircle size (shadcn.io)
--thicknessStroke width (shadcn.io)

CircularProgressRootProvider

Takes the API from useCircularProgress. Same baked ring as CircularProgress.

PropTypeDefaultDescription
valueUseProgressReturnrequiredReturn value of useCircularProgress().
asChildbooleanfalseRender the child element instead of a div.
classNamestring-Class names on the root.
sizenumber32Circle size in pixels.
thicknessnumber4Stroke width in pixels.

Pass defaultValue, min, max, formatOptions, and other machine options to useCircularProgress(), not to the provider.

CircularProgressTrack

The SVG circle (Progress.Circle) with track and range. Baked into the root; export remains for custom composition. Renders an svg with role="progressbar".

PropTypeDefaultDescription
asChildbooleanfalseMerge onto a single child.
classNamestring-Class names on the svg.
sizenumber32Circle size in pixels.
thicknessnumber4Stroke width in pixels.
AttributeDescription
data-slotcircular-progress-circle
data-scopeprogress
data-partcircle
data-state"loading", "complete", or "indeterminate"
data-maxMaximum value
data-orientation"horizontal" or "vertical"

Track circle: data-slot="circular-progress-track", data-part="circle-track". Range circle: data-slot="circular-progress-range", data-part="circle-range", data-state as above.

aria-valuemin / aria-valuemax / aria-valuenow live on the svg. aria-valuenow is omitted when indeterminate. aria-label is the formatted value (or "loading...").

CircularProgressValue

Formatted value text (ValueText). Renders a span with aria-live="polite". Positioned over the ring (size-(--size) at the bottom of the root).

PropTypeDefaultDescription
asChildbooleanfalseMerge onto a single child.
classNamestring-Class names on the text.
AttributeDescription
data-slotcircular-progress-value
data-scopeprogress
data-partvalue-text

CircularProgressLabel

Visible label above the ring. Renders a span.

PropTypeDefaultDescription
asChildbooleanfalseMerge onto a single child.
classNamestring-Class names on the label.
AttributeDescription
data-slotcircular-progress-label
data-scopeprogress
data-partlabel
data-orientation"horizontal" or "vertical"

CircularProgressView

Renders children only when state matches the current progress state. Renders a span.

PropTypeDefaultDescription
state"loading" | "complete" | "indeterminate"requiredWhich state this view is for.
asChildbooleanfalseMerge onto a single child.
classNamestring-Class names on the view.
AttributeDescription
data-slotcircular-progress-view
data-scopeprogress
data-partview
data-stateThe state prop

Linear-only parts (Track / Range as divs) belong on Progress.

useCircularProgress

Creates the progress API for CircularProgressRootProvider. Accepts the same machine options as CircularProgress except layout-only props (size, thickness, indeterminate, asChild, className).

const progress = useCircularProgress({ defaultValue: 50 });
progress.setToMax();
progress.setValue(null);

CircularProgressContext / useCircularProgressContext

Render-prop or hook access. Use inside CircularProgress or CircularProgressRootProvider.

PropertyTypeDescription
valuenumber | nullCurrent value (null when indeterminate).
valueAsStringstringFormatted value.
percentnumber0–100 percent of minmax.
percentAsStringstringFormatted percent.
min / maxnumberBounds.
indeterminatebooleanWhether value is null.
setValue(value: number | null) => voidSet the value.
setToMax / setToMin() => voidJump to a bound.

CircularProgressContext children: (context) => ReactNode.

Accessibility

Complies with the progressbar role. The SVG circle is the progressbar. Determinate values expose aria-valuemin, aria-valuemax, and aria-valuenow. Indeterminate omits aria-valuenow. CircularProgressValue uses aria-live="polite". Prefer a visible CircularProgressLabel when the percent alone is not enough context.

Keyboard support

The circular progress is not a keyboard control. It is a status indicator. Focus stays on whatever started the work (button, form, upload).