Progress
Communicates how far along a task is — an upload, an install, a multi-step
flow. Built on Base UI Progress, which sizes the indicator from value and
announces the percentage to assistive tech. For a static measurement within a
range (disk, capacity), use Meter instead.
Import
import { Progress } from "@photon-ai/pho-ui/components/progress";
Basic
Set value (0–100) on Progress.Root. Compose a header row with
Progress.Label and Progress.Value.
<Progress.Root value={60}>
<div className="flex items-center justify-between">
<Progress.Label>Uploading…</Progress.Label>
<Progress.Value />
</div>
<Progress.Track>
<Progress.Indicator />
</Progress.Track>
</Progress.Root>
Indeterminate
Pass value={null} when the duration is unknown — the bar shows an
indeterminate state instead of a fixed fill.
<Progress.Root value={null}>
<Progress.Track>
<Progress.Indicator />
</Progress.Track>
</Progress.Root>
Formatting the value
The range isn’t fixed to 0–100. Set max to count real units and give
Progress.Value a render function to label them (“3 / 8”); getAriaValueText
supplies the spoken version (“3 of 8 files uploaded”). Pass format for a
locale-aware Intl.NumberFormat reading (percent, bytes) instead.
<Progress.Root
value={3}
max={8}
getAriaValueText={(_, value) => `${value} of 8 files uploaded`}
>
<div className="flex items-center justify-between">
<Progress.Label>Uploading files</Progress.Label>
<Progress.Value>{(_, value) => `${value} / 8`}</Progress.Value>
</div>
<Progress.Track>
<Progress.Indicator />
</Progress.Track>
</Progress.Root>
Anatomy
- Progress.Root — owns
value(and optionalmax);null= indeterminate - Progress.Label — names the task
- Progress.Value — renders the formatted percentage
- Progress.Track — the rail
- Progress.Indicator — the fill; width is managed by Base UI
Props
Every part also accepts className and render (Base UI’s prop for swapping
the underlying element).
Progress.Root (<div role="progressbar">)
- value —
number · null(required) — current value;nullrenders the indeterminate state - min —
number(default0) — start of the range - max —
number(default100) — end of the range - format —
Intl.NumberFormatOptions— howProgress.Valueformats the number (percent, bytes, …) - locale —
Intl.LocalesArgument— locale for formatting; defaults to the runtime locale - getAriaValueText —
(formattedValue, value) => string— human-readable text foraria-valuetext - aria-valuetext —
string— static value text for assistive tech
Progress.Value (<span>)
- children —
(formattedValue, value) => ReactNode— render-prop override; defaults to the formatted percentage
Progress.Label (<span>), Progress.Track (<div>), Progress.Indicator (<div>) take only the common className / render; the indicator’s inline size is set by Base UI from value.
Accessibility
- Semantics —
Progress.Rootrendersrole="progressbar"witharia-valuemin,aria-valuemax, andaria-valuenow(omitted while indeterminate), exposing the value to screen readers. - Labelling — a
Progress.Labelis linked viaaria-labelledby. With no visible label, passaria-labelonProgress.Rootso the bar isn’t anonymous;getAriaValueTextsupplies a friendlier reading (“3 of 5 files”). - Not interactive — it’s a status indicator, not a control: nothing to focus and no keyboard interaction.
- Announcements — the
progressbarrole reports the current value but isn’t a live region, so rapid updates aren’t pushed to assistive tech. For a long silent task, add your own polite status text at meaningful milestones.
Best practices
- Use
Progressfor task completion that moves forward over time; useMeterfor a fixed reading like storage used. - Show a label so the bar isn’t an unlabeled sliver — what is loading matters.
- Switch to indeterminate (
value={null}) only when you truly can’t estimate progress; a moving-but-honest bar beats a fake one.