Circular progress
Free · MITSVG ring or 270° gauge whose arc springs to the value via stroke-dashoffset, with a rolling percentage or custom label in the center.
Installation
$ npx shadcn@latest add https://goodui.dev/r/circular-progress.jsonThe file lands in components/goodui/circular-progress.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/circular-progress works too.
Usage
import { CircularProgress } from "@/components/goodui/circular-progress"
import { NumberFlow } from "@/components/goodui/number-flow"
export function RevenueGoal({ revenue, goal }: { revenue: number; goal: number }) {
return (
<CircularProgress
variant="gauge"
value={revenue}
max={goal}
size={152}
strokeWidth={12}
indicatorClassName="text-[var(--chart-2)]"
aria-label="November revenue goal"
aria-valuetext={`${revenue} of ${goal} dollars`}
label={
<NumberFlow
value={revenue}
format={{ style: "currency", currency: "USD", notation: "compact", maximumFractionDigits: 1 }}
className="font-semibold tracking-tight"
/>
}
sublabel="of $25k goal"
/>
)
}When to use
- A single percentage that deserves a visual: goal reached, utilization, profile completion.
- Revenue or target gauges on a dashboard (variant="gauge") with your own center label.
- Small per-row indicators, such as each staff member's schedule load at 44px.
- Quota with a used / limit readout and warnings: use meter.
- Long task progress in a form or dialog: use progress, a linear bar reads more precisely.
- Comparing several values against each other: use a bar chart; rings are hard to compare.
| If you need | Use |
|---|---|
| Plain percentage | variant="ring" (default) with value |
| Speedometer-style goal | variant="gauge" with max={goal} |
| Money or other units in the center | label={<NumberFlow … />} plus your own aria-valuetext |
| Tiny inline ring | size={44} strokeWidth={4} label={null} |
| Color by status | indicatorClassName="text-emerald-500" (the arc uses currentColor) |
Examples
Utilization ring with rolling percentage
<CircularProgress value={72} size={136} sublabel="booked" aria-label="Chair utilization this week" />Per-row load indicator
<CircularProgress
value={member.load}
size={44}
strokeWidth={4}
indicatorClassName={member.load >= 90 ? "text-rose-500" : member.load >= 75 ? "text-amber-500" : "text-emerald-500"}
aria-label={`${member.name} schedule load`}
/>API reference
| Prop | Type | Default |
|---|---|---|
| value* Current value on the 0 to max scale; the arc springs to it. | number | — |
| aria-label Defines a string value that labels the current element. Accessible name for the progress bar. | string | "Progress" |
| max Value that fills the whole ring or gauge. | number | 100 |
| size Diameter in px. | number | 120 |
| strokeWidth Ring thickness in px. Defaults to 9% of the size. | number | — |
| variant "ring" is a full circle, "gauge" is a 270° arc open at the bottom. | "ring" | "gauge" | "ring" |
| label Center content. Defaults to the rolling percentage. Pass null to hide it. | React.ReactNode | — |
| sublabel Small line under the center label. | React.ReactNode | — |
| indicatorClassName Classes for the arc; it is drawn with currentColor, e.g. "text-emerald-500". | string | — |
| trackClassName Classes for the track; drawn with currentColor. | string | — |
Also accepts every <div> attribute.
Accessibility
- The root is role="progressbar" with aria-valuemin 0, aria-valuemax max, aria-valuenow and aria-valuetext as a rounded percent.
- aria-label defaults to "Progress"; pass a specific one ("Chair utilization this week").
- The SVG and the center label are aria-hidden. When the label shows something other than a percent, pass your own aria-valuetext; it overrides the default.
Motion
- On mount the arc springs from empty to the value via stroke-dashoffset (stiffness 70, damping 18, mass 1), and again on every change.
- The default center label is number-flow, so the percentage digits roll with the arc.
- At 0 the arc fades out in 150 ms, because a round cap would still paint a dot.
- Reduced motion: the arc and digits jump to the value with duration 0.
Notes for AI
Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.
- value is on the 0 to max scale (default max 100); for a goal pass max={goal} instead of computing a percent.
- Color the arc with a text-* class in indicatorClassName and the track with trackClassName; the stroke uses currentColor, so bg-* does nothing.
- label={null} hides the center; leaving label undefined shows the rolling percentage.
- size is a number in px, not a Tailwind class; strokeWidth defaults to 9% of size (at least 3).
- With a custom label, also pass aria-valuetext so screen readers hear the same thing.
FAQ
Where does the gauge's gap go?
The gauge is a 270° arc that starts at 7:30 and ends at 4:30, so the open part is at the bottom. The center label is nudged down slightly to sit in the middle of the arc.
How big is the center text?
It scales with size: 20% of the diameter for the label (at least 11px) and 9% for the sublabel (at least 10px).
Related
Linear progress in four variants: spring-filled determinate, indeterminate double sweep, moving stripes and segmented steps.
Labeled usage meter like "SMS credits 7,420 / 10,000" whose fill springs to the new value and shifts to amber and rose near the limit, with optional per-seat segments.
Digits roll to the new value with locale formatting, prefixes and suffixes.
KPI card with seven variants, tones, shadows and sizes: count-up or rolling NumberFlow values, auto-toned delta badges (invertible for no-show rates), line, area or bar sparklines that draw in, icons, footers and a skeleton state.