0

Progress Ring

A task's progress drawn as a ring, with support for an indeterminate state.

x
import { ProgressRing } from "@/components/ui/progress-ring"

export default function ProgressRingDemo() {

Installation

bunx --bun shadcn@latest add @ondo-ui/progress-ring

Usage

import { ProgressRing, ProgressRingLabel } from "@/components/ui/progress-ring"
<ProgressRing value={72} min={0} max={100} size="sm | default | lg" variant="default">
  <ProgressRingLabel>Uploading</ProgressRingLabel>
</ProgressRing>

ProgressRing builds on Base UI's Progress, so it is announced as a progressbar. The dial always renders; anything you pass as children sits under it, which is where ProgressRingLabel goes.

format shapes the centered number. When the center needs something a number format cannot express, formatValue replaces it outright.

The arc is a conic gradient masked into a ring — two elements, no SVG — and it takes its color from currentColor, so variant (or any text-color utility in className) recolors it.

The sweep animates rather than jumping because the component registers its fraction with @property, which is what makes a number inside a gradient interpolable. That registration ships inside the component file — React hoists it into <head> once, however many rings are on the page — so there is nothing to add to your CSS.

Examples

Sizes

x
x
x
import { ProgressRing } from "@/components/ui/progress-ring"

export default function ProgressRingSizes() {

Indeterminate

Pass value={null} when the task has started but its progress is unknowable. The arc becomes a spinner rather than claiming a number it does not have, and the ring reports itself as indeterminate to assistive tech.

The center is empty in this state, because there is no honest number to print. Use formatValue if you want something there anyway.

Connectingx
Waiting for serverx
"use client"

import { ProgressRing, ProgressRingLabel } from "@/components/ui/progress-ring"

Variants

variant picks the arc color from the theme's semantic set — useful for signalling that a task stalled, is retrying, or finished cleanly.

Stalledx
Syncingx
Retryingx
Donex
import { ProgressRing, ProgressRingLabel } from "@/components/ui/progress-ring"

export default function ProgressRingVariants() {

Label and center

ProgressRingLabel names the task under the dial. format shapes the centered number; formatValue replaces it when a number format cannot say what is needed — a step count, for instance.

formatValue is a function prop, so the component passing it has to be a Client Component.

Uploadingx
Steps donex
"use client"

import { ProgressRing, ProgressRingLabel } from "@/components/ui/progress-ring"

An animated center

formatValue can return anything, so hand the reading to NumberCount and the digits roll between readings instead of snapping. Base UI marks this part aria-hidden, so the animation is purely visual — the announced value still comes from the root — and NumberCount honours prefers-reduced-motion.

formatValue receives the formatted string and the raw number, so formatting is repeated on NumberCount rather than inherited from ProgressRing. Mind the difference between the two: on a 0–100 scale use suffix="%"; format={{ style: "percent" }} is for a value that is already a 0–1 fraction, and would read 2400% here.

Uploadingx
"use client"

import * as React from "react"

Returning null for a null value keeps the indeterminate ring's center empty while the arc spins, which is what Base UI does by default.

API reference

ProgressRing

PropTypeDefault
valuenumber | null
minnumber0
maxnumber100
size"sm" | "default" | "lg""default"
variant"default" | "info" | "success" | "warning" | "destructive""default"
formatIntl.NumberFormatOptions
formatValue(formatted: string | null, value: number | null) => ReactNode
localeIntl.LocalesArgumentruntime locale
getAriaValueText(formatted: string | null, value: number | null) => string

Plus the rest of Base UI's Progress.Root props.

The root carries Base UI's state attributes — data-indeterminate, data-progressing, and data-complete — so you can style the ring off its own progress without tracking it yourself.

ProgressRingDial, ProgressRingTrack, ProgressRingIndicator

The ring and its parts. ProgressRing composes them for you; reach for them only to restyle the ring or to put something other than the value in the middle.

ProgressRingValue, ProgressRingLabel

Base UI's Progress.Value and Progress.Label with ondo styling. ProgressRingValue takes a render function ((formatted, value) => ReactNode) when format alone is not enough.