Progress
Free · MITLinear progress in four variants: spring-filled determinate, indeterminate double sweep, moving stripes and segmented steps.
Installation
$ npx shadcn@latest add https://goodui.dev/r/progress.jsonThe file lands in components/goodui/progress.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/progress works too.
Usage
import { Progress } from "@/components/goodui/progress"
export function ImportStatus({ imported, total }: { imported: number; total: number }) {
return (
<div className="flex flex-col gap-5 rounded-xl border bg-card p-5">
<Progress label="Uploading client-photos.zip" value={64} showValue />
<Progress
variant="striped"
label={`Importing clients · ${imported.toLocaleString()} of ${total.toLocaleString()}`}
value={imported}
max={total}
showValue
size="lg"
indicatorClassName="bg-emerald-500"
/>
</div>
)
}When to use
- Uploads, imports and exports where you know how far along the work is.
- Background work of unknown length ("Syncing Google Calendar…"): the indeterminate variant.
- Onboarding or setup flows where a few equal steps should read as segments.
- Quota or plan usage that never finishes: use meter, which warns near the limit.
- A named, clickable list of steps the user moves through: use stepper.
- Short waits under a second or loading a whole panel: use spinners or skeleton-auto.
| If you need | Use |
|---|---|
| Known progress (upload, import) | variant="determinate" with value and max |
| Unknown duration | variant="indeterminate" (value is ignored) |
| Long job that should look busy while it moves | variant="striped" |
| A handful of equal steps | variant="steps" steps={n} max={n} value={finished} |
| Thin bar in a header or card | size="sm" |
Examples
Unknown duration, then done
<Progress
variant={syncing ? "indeterminate" : "determinate"}
value={100}
label={syncing ? "Syncing Google Calendar…" : "Calendar synced"}
size="sm"
indicatorClassName="bg-sky-500"
/>Onboarding steps
const steps = ["Business details", "Services and prices", "Opening hours", "Invite your team", "Go live"]
<Progress variant="steps" steps={steps.length} max={steps.length} value={finished} label="Studio setup" showValue />API reference
| Prop | Type | Default |
|---|---|---|
| value Current value, 0 to max. Ignored by the indeterminate variant. | number | 0 |
| max Value that counts as complete. For steps, set it to the step count so value is the number of finished steps. | number | 100 |
| variant determinate: spring fill. indeterminate: looping sweep for unknown duration. striped: fill with moving stripes. steps: one segment per step. | "determinate" | "indeterminate" | "striped" | "steps" | "determinate" |
| steps Number of segments for the steps variant. | number | 4 |
| size Bar height: sm 4px, md 8px, lg 12px. | "sm" | "md" | "lg" | "md" |
| label Optional label above the bar. | React.ReactNode | — |
| showValue Show the percentage (or "step x of n") next to the label. | boolean | false |
| indicatorClassName Classes for the filled part, e.g. "bg-emerald-500". | string | — |
| labels Text for built-in controls and screen readers; pass translations here. | Partial<ProgressLabels> | — |
Also accepts every <div> attribute.
Accessibility
- Renders role="progressbar" with aria-valuemin, aria-valuemax, aria-valuenow and aria-valuetext ("64%" or "Step 3 of 5").
- With a label, the bar is named by it through aria-labelledby; without one it gets aria-label="Progress". Both built-in strings go through labels (progress, stepOf).
- The indeterminate variant drops the value attributes and sets aria-busy="true".
Motion
- Determinate, striped and each step segment translate the fill on a spring (stiffness 110, damping 22, mass 0.8); at 0 the fill fades out after 300 ms.
- Indeterminate: two bars sweep across on a 1.6 s CSS loop, the second starting 0.8 s later.
- Striped: the stripes slide on a 0.8 s linear loop while the fill springs to the value.
- Reduced motion: fills jump with duration 0, stripes stop moving, and the indeterminate sweep becomes a static full-width bar at 45% opacity.
- Only transform and opacity animate. The indeterminate and striped loops are CSS keyframes, so no JavaScript runs per frame.
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), not 0 to 1.
- For steps, pass steps and max as the same number and value as the count of finished steps; value 2 of 5 reads "Step 3 of 5".
- Color the fill with indicatorClassName ("bg-emerald-500"), not className, which goes on the wrapper.
- showValue is ignored by the indeterminate variant; value is ignored too.
- labels={{ progress: "Fortschritt", stepOf: (s, n) => `Schritt ${s} von ${n}` }} translates the fallback name and the steps text. Under RTL the track is mirrored, so bars fill from the right.
FAQ
How do I switch from indeterminate to a real value?
Change variant from "indeterminate" to "determinate" and pass value; the fill springs in from the start of the track.
Do the step segments fill one after another?
Each segment springs on its own share of the value at the same time. When value goes up by one step, only that segment fills, so stepping through looks sequential.
Related
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.
SVG ring or 270° gauge whose arc springs to the value via stroke-dashoffset, with a rolling percentage or custom label in the center.
Horizontal or vertical stepper where the connecting line fills between steps, numbers morph into drawn checkmarks, a current-step ring glides to the active step, errors shake, and completed steps are clickable.
Card or overlay that ticks through steps like "Reserving slot…" with spinners, drawn checkmarks and a filling rail, ending on an animated success state.