Progress
A determinate or indeterminate bar for work whose size is known.
@luc/uiExamples
A bar that is actually progressing
The value lives in the caller, and the bar's own width transition is what moves — no second animation is written beside the component.
Ingest paused
62 %
const [value, setValue] = useState(62);
<Progress value={value} aria-label="Sector ingest" />At the ends, and in the middle
Zero is a value, not an absence: a bar at 0 per cent is determinate and announces `aria-valuenow="0"`.
Not started
0 %
Running
62 %
Finished
100 %
<Progress value={0} aria-label="Not started" />
<Progress value={62} aria-label="Running" />
<Progress value={100} tone="positive" aria-label="Finished" />Indeterminate
Omitting `value` is the honest answer when the length is genuinely unknown — and it is the one mode where the component owns the motion.
Reconnecting to the stream
—
Connecting to the provider
—
{/* No value: the sweep, and no aria-valuenow to lie about. */}
<Progress aria-label="Reconnecting to the stream" />Two sizes, four tones
`size` is the bar's height, not the label's: `sm` is the one that fits inside a table row or a toolbar.
Ingest · accent · md
48 %
Validated · positive · md
91 %
Quota · warning · sm
78 %
Rejected · negative · sm
12 %
<Progress value={48} aria-label="Ingest" />
<Progress value={91} tone="positive" aria-label="Validated" />
<Progress value={78} tone="warning" size="sm" aria-label="Quota" />
<Progress value={12} tone="negative" size="sm" aria-label="Rejected" />In a task row
Where a bar actually lives: beside a status and a sentence about what is running, with the action that stops it.
Sector ingest
Pending18 of 44 sectors · about 6 minutes left
Started 12:04 UTC · writes to luc-data/research
<Progress value={41} aria-label="Sector ingest" />Usage
import { Progress } from "@luc/ui";<Progress value={62} aria-label="Tasks complete" />Omit `value` for the indeterminate sweep; the bar then exposes the role without a value, which is what the pattern asks for.
Package setup — peer requirements, the stylesheet and the provider — is on installation.
API
Read from the published TypeScript API, so a prop that is not listed here does not exist and a default that is listed is the one the compiler sees.
Three props: the value (or its absence), the tone and the size. `value={null}` and no `value` at all mean the same thing — the indeterminate sweep.
| Export | Kind | Signature |
|---|---|---|
Progress | 6 documented props | ({ className, value, tone, size, ...props }: ProgressProps) => import("@types/react").JSX.Element |
Progress props
Type: ProgressProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
asChild?inherited | boolean | undefined | — | |
getValueLabel?inherited | ((value: number, max: number) => string) | undefined | — | |
max?inherited | number | undefined | — | |
size? | "sm" | "md" | undefined | — | |
tone? | "warning" | "accent" | "positive" | "negative" | undefined | — | |
value? | number | null | undefined | — | 0–100. Omit for an indeterminate bar. |
Guidelines
Do
- Use a determinate bar when the work has a known size, and the indeterminate sweep when it genuinely does not.
- Put the number beside the bar in the caller's own type — the component draws the track, not the label.
- Let the width transition express the change; the reader is watching the value, not the surface.
Don't
- Do not show a percentage you cannot compute — an invented number is worse than an honest sweep.
- Do not use a progress bar for a wait with no measurable end; that is a spinner.
- Do not add a second animation to the indeterminate sweep: it is already the one motion in the component.
Accessibility
Radix owns the value semantics: a determinate bar exposes `role="progressbar"` with `aria-valuenow`, `aria-valuemin` and `aria-valuemax`, and an indeterminate one exposes the role without a value. The bar has no name of its own, so give it one — `aria-label`, or `aria-labelledby` pointing at the heading it belongs to — and the indeterminate sweep is marked `data-motion-essential`, so it keeps meaning under `prefers-reduced-motion` as a slow pulse rather than freezing.
Family
Feedback
What the product says when something happened, is happening, or did not.
10 documented entries in this family. Open the family index
Related