- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Collapsible
- Combobox
- Command
- Context Menu
- Desktop Window
- Dialog
- Drawer
- Dropdown Menu
- Empty
- Field
- Frame
- Heading
- Hover Card
- Input
- Input Group
- Input OTP
- Item
- Kbd
- Label
- Live Waveform
- Marker
- Menubar
- Message
- Message Scroller
- Meter
- Meter Ring
- Native Select
- Navigation Menu
- NumberCount
- Pagination
- Popover
- Progress
- Progress Ring
- Radio Group
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Spinner
- Switch
- Table
- Tabs
- Textarea
- Timeline
- Toast
- Toggle
- Toggle Group
- Tooltip
A task's progress drawn as a ring, with support for an indeterminate state.
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#
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.
"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.
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.
"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.
"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#
| Prop | Type | Default |
|---|---|---|
value | number | null | – |
min | number | 0 |
max | number | 100 |
size | "sm" | "default" | "lg" | "default" |
variant | "default" | "info" | "success" | "warning" | "destructive" | "default" |
format | Intl.NumberFormatOptions | – |
formatValue | (formatted: string | null, value: number | null) => ReactNode | – |
locale | Intl.LocalesArgument | runtime 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.