Skip to content

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.

Uploading
x
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

PartDescription
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:

Onboarding
x
Storage
x
<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.

Connecting
x
No value, so the bar says only that something is running — not how far along it is.
<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

Disk used
x
Backup
x
Monthly quota
x
Seats filled
x
<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.Label names the bar. Without it, give Progress.Root an aria-label — a bar announced only as “40%” says nothing about what is at 40%.
  • Progress.Value is hidden from screen readers. The root already announces the value, so the text is not read twice.
  • The announced value follows format. Override it with getAriaValueText when 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: reduce the 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>