Progress
Shows how far a task has come, or that it is underway when its length is unknown.
import {
Progress,
ProgressLabel,
ProgressValue,
} from "@/components/ui/progress"
export default function ProgressDemo() {
return (
<Progress value={64} className="w-full max-w-xs">
<ProgressLabel>Uploading assets</ProgressLabel>
<ProgressValue />
</Progress>
)
}Anatomy
- 1LabelSays what is in progress. It also names the bar for screen readers.
- 2TrackThe whole task, in a muted tint.
- 3ValueThe percentage, in tabular figures so it doesn't jitter as it changes.
- 4IndicatorThe completed share. It animates between values, and sweeps across the track when the value is unknown.
Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/progress.jsonUsage
import { Progress, ProgressLabel, ProgressValue } from "@/components/ui/progress"<Progress value={64}>
<ProgressLabel>Uploading assets</ProgressLabel>
<ProgressValue />
</Progress>Progress renders the track and indicator for you. Children — typically a label and a value — sit on the line above the bar.
Examples
Sizes
sm (4px) fits inside lists and table cells, default (6px) suits most forms and cards, and lg (10px) is for a progress bar that is the focus of the view.
import {
Progress,
ProgressLabel,
ProgressValue,
} from "@/components/ui/progress"
export default function ProgressSizes() {
return (
<div className="flex w-full max-w-sm flex-col gap-6">
<Progress value={30} size="sm">
<ProgressLabel>Small</ProgressLabel>
<ProgressValue />
</Progress>
<Progress value={55}>
<ProgressLabel>Default</ProgressLabel>
<ProgressValue />
</Progress>
<Progress value={80} size="lg">
<ProgressLabel>Large</ProgressLabel>
<ProgressValue />
</Progress>
</div>
)
}Indeterminate
Pass value={null} when you know work is happening but not how much remains. A segment sweeps across the track until you switch to a determinate value.
import { Progress, ProgressLabel } from "@/components/ui/progress"
export default function ProgressIndeterminate() {
return (
<Progress value={null} className="w-full max-w-xs">
<ProgressLabel>Preparing your export…</ProgressLabel>
</Progress>
)
}Live updates
The indicator animates between values with the slow duration and standard easing, so frequent updates read as smooth motion rather than jumps.
"use client"
import * as React from "react"
import { RotateCcwIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Progress,
ProgressLabel,
ProgressValue,
} from "@/components/ui/progress"
export default function ProgressLive() {
const [value, setValue] = React.useState(0)
const [run, setRun] = React.useState(0)
React.useEffect(() => {
// Each run counts up from zero; Restart resets the value and starts a new run.
let progress = 0
const timer = setInterval(() => {
progress = Math.min(100, progress + Math.round(Math.random() * 12 + 4))
setValue(progress)
if (progress === 100) clearInterval(timer)
}, 450)
return () => clearInterval(timer)
}, [run])
return (
<div className="flex w-full max-w-xs flex-col items-end gap-4">
<Progress value={value} className="w-full">
<ProgressLabel>
{value === 100 ? "Build complete" : "Building…"}
</ProgressLabel>
<ProgressValue />
</Progress>
<Button
variant="outline"
size="xs"
onClick={() => {
setValue(0)
setRun((n) => n + 1)
}}
>
<RotateCcwIcon data-icon="inline-start" />
Restart
</Button>
</div>
)
}File uploads
In lists, use the sm size and let the file name be the label. Swap the value for a confirmation when an item finishes, so completed rows stop drawing the eye.
- brand-guidelines.pdfDone4.2 MB
- hero-illustration.png2.8 MB
- product-walkthrough.mp4148 MB
import {
CircleCheckIcon,
FileTextIcon,
ImageIcon,
FileVideoIcon,
} from "lucide-react"
import {
Progress,
ProgressLabel,
ProgressValue,
} from "@/components/ui/progress"
const files = [
{
name: "brand-guidelines.pdf",
size: "4.2 MB",
value: 100,
icon: FileTextIcon,
},
{ name: "hero-illustration.png", size: "2.8 MB", value: 72, icon: ImageIcon },
{
name: "product-walkthrough.mp4",
size: "148 MB",
value: 18,
icon: FileVideoIcon,
},
]
export default function ProgressFileUpload() {
return (
<ul className="flex w-full max-w-md flex-col divide-y rounded-xl border bg-card shadow-raised">
{files.map((file) => {
const Icon = file.icon
const done = file.value === 100
return (
<li key={file.name} className="flex items-start gap-3 p-4">
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg border bg-surface text-muted-foreground">
<Icon className="size-4" />
</span>
<Progress value={file.value} size="sm" className="min-w-0 flex-1">
<ProgressLabel className="truncate">{file.name}</ProgressLabel>
{done ? (
<span className="ml-auto flex items-center gap-1 text-sm text-success-11">
<CircleCheckIcon className="size-4" />
Done
</span>
) : (
<ProgressValue />
)}
<span className="order-last w-full text-xs text-muted-foreground">
{file.size}
</span>
</Progress>
</li>
)
})}
</ul>
)
}Custom value
ProgressValue accepts a render function for units other than percent. When you change the visible format, give assistive technology the same words with getAriaValueText.
"use client"
import {
Progress,
ProgressLabel,
ProgressValue,
} from "@/components/ui/progress"
const used = 7.2
const total = 10
const format = (value: number) => value.toLocaleString("en-US")
export default function ProgressCustomValue() {
return (
<Progress
value={used}
max={total}
getAriaValueText={() =>
`${format(used)} of ${format(total)} gigabytes used`
}
className="w-full max-w-xs"
>
<ProgressLabel>Storage</ProgressLabel>
<ProgressValue>
{() => `${format(used)} GB of ${format(total)} GB`}
</ProgressValue>
</Progress>
)
}<Progress
value={7.2}
max={10}
getAriaValueText={() => "7.2 of 10 gigabytes used"}
>
<ProgressLabel>Storage</ProgressLabel>
<ProgressValue>{() => "7.2 GB of 10 GB"}</ProgressValue>
</Progress>Guidelines
When to use
- For tasks that take longer than about a second and report measurable progress: uploads, exports, imports, builds.
- For quantities against a limit — storage, seats, quota — where the bar is a gauge rather than a timer.
When not to use
- For short or unmeasurable waits inside a control — use a Spinner or the Button's
loadingstate. - For content that is still loading — use a Skeleton that previews the layout.
- For steps in a flow. A progress bar measures work, not navigation.
Skeleton, spinner or progress?
Be honest
Never let a determinate bar stall at 99% or jump backward. If you can't estimate progress reliably, use the indeterminate state and say what's happening in the label.
Accessibility
- Renders
role="progressbar"witharia-valuemin,aria-valuemaxandaria-valuenow. The indeterminate state omitsaria-valuenow, which screen readers announce as busy. ProgressLabelis wired to the bar witharia-labelledby. When you can't show a label, passaria-labeltoProgressinstead — a progress bar must always have a name.aria-valuetextis generated from the formatted value. Override it withgetAriaValueTextwhenever the visible value isn't a percentage.- Progress bars aren't live regions: screen readers read the value when people navigate to it, not on every change. Announce milestones — Upload complete — with a Toast or a
role="status"message. - Under
prefers-reduced-motion, value changes and the indeterminate sweep stop animating.
API reference
Progress
The root. Renders a <div> containing your children followed by ProgressTrack and ProgressIndicator. Accepts every prop of Base UI's Progress.Root.
ProgressLabel
The accessible name of the bar. Renders a <span>.
ProgressValue
Displays the formatted value, right-aligned. Renders a <span>.
ProgressTrack and ProgressIndicator
Rendered by Progress automatically. They are exported for custom compositions — for example, a segmented meter that renders several indicators.