Progress
A bar that fills as a task advances — an upload, an import, a multi-step form — and a moving sliver when there is no way to know how far along it is.
import { Progress } from '@ontoui/react';
<Progress.Root value={40}> <Progress.Label>Uploading</Progress.Label> <Progress.Value /> <Progress.Track> <Progress.Indicator /> </Progress.Track></Progress.Root>;Anatomy
| Part | Description |
|---|---|
Progress.Root | <div role="progressbar"> — holds the value and announces it. |
Progress.Label | <span> — names the task. Becomes the bar’s accessible name. |
Progress.Value | <span> — the value as text, for sighted users. |
Progress.Track | <div> — the rail the indicator runs along. |
Progress.Indicator | <div> — the filled part. Sized from the value; must sit in the track. |
The label and the value share the row above the bar, and the bar spans the full width beneath them. Rows that hold nothing collapse, so a bar on its own carries no stray space above it.
Progress is for a task whose progress is being reported. For a static quantity that happens
to be a fraction of a whole — a rating, a score, a disk gauge that is not currently changing —
a progress bar is the wrong control: it tells assistive technology that something is underway.
Value
value is required, and it is always controlled: a progress bar reflects work happening
elsewhere, so there is nothing for it to hold on its own.
<Progress.Root value={uploadedBytes} max={totalBytes}>Progress.Value renders the value as text. By default it reads as a percentage of 100 — the
scale min and max already default to. On any other scale, pass format and say what the
number means:
<Progress.Root value={3} max={5} format={{ style: 'decimal' }}> <Progress.Label>Onboarding</Progress.Label> <Progress.Value>{(formatted) => `${formatted} of 5 steps`}</Progress.Value> <Progress.Track> <Progress.Indicator /> </Progress.Track></Progress.Root>format takes Intl.NumberFormat options and drives both the rendered text and the announced
one, so a bar measured in megabytes or steps says so out loud too. The render prop receives the
formatted string and the raw number, for wording the formatter cannot produce on its own.
The indicator is sized from the value against min and max whatever the formatting, so a bar
showing 840 MB of 1,024 MB still fills to the right place.
Indeterminate
value={null} says the task is running and its progress is unknown. The bar drops
aria-valuenow, announces itself as indeterminate, and animates a sliver along the track.
<Progress.Root value={null}> <Progress.Label>Connecting</Progress.Label> <Progress.Track> <Progress.Indicator /> </Progress.Track></Progress.Root>Leave Progress.Value out here, or give it a render prop — it renders nothing while there is
no value, and the render prop receives the string "indeterminate" in place of a number.
Switching between null and a number as soon as the first byte lands is the normal pattern:
start indeterminate, become determinate the moment there is something to measure.
Color
<Progress.Root value={91} color="warning">Color is decorative — a screen reader hears the same value at any tint. Keep the meaning in the
label: Monthly quota next to an amber bar, not an amber bar on its own.
Styling
The track is a plain block, so its height, radius and colors are ordinary CSS. Pass a
className to the part you want to change:
<Progress.Track className="thin"> <Progress.Indicator /></Progress.Track>.thin { height: 3px;}Every part carries data-progressing, data-complete or data-indeterminate, so a bar can
restyle itself on completion without any of that state being threaded through React.
.thin[data-complete] { opacity: 0.6;}Accessibility
Progress.Labelnames the bar. Without it, giveProgress.Rootanaria-label— a bar announced only as “40%” says nothing about what is at 40%.Progress.Valueis hidden from screen readers. The root already announces the value, so the text is not read twice.- The announced value follows
format. Override it withgetAriaValueTextwhen the formatted number needs words around it, so the bar reads as “40% uploaded” rather than “40%”. - Motion is reduced, not removed. Under
prefers-reduced-motion: reducethe indeterminate bar stops travelling and pulses in place instead — a bar frozen at zero would read as stalled.
API Reference
Progress.Root
| Prop | Type | Default |
|---|---|---|
children? | ReactNode | — |
className? | string | — |
style? | CSSProperties | — |
value | number | null | — |
min? | number | 0 |
max? | number | 100 |
format? | NumberFormatOptions | — |
locale? | LocalesArgument | the runtime locale |
getAriaValueText? | (formattedValue: string | null, value: number | null) => string | — |
color? | "default" | "success" | "warning" | "danger" | 'default' |
ref? | Ref<HTMLDivElement> | — |
Progress.Label
| Prop | Type | Default |
|---|---|---|
children? | ReactNode | — |
className? | string | — |
style? | CSSProperties | — |
id? | string | — |
ref? | Ref<HTMLDivElement> | — |
Progress.Value
| Prop | Type | Default |
|---|---|---|
className? | string | — |
style? | CSSProperties | — |
children? | (formattedValue: string | null, value: number | null) => ReactNode | — |
ref? | Ref<HTMLDivElement> | — |
Progress.Track
| Prop | Type | Default |
|---|---|---|
children? | ReactNode | — |
className? | string | — |
style? | CSSProperties | — |
ref? | Ref<HTMLDivElement> | — |
Progress.Indicator
| Prop | Type | Default |
|---|---|---|
className? | string | — |
style? | CSSProperties | — |
ref? | Ref<HTMLDivElement> | — |