Photon Design System

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.

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

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

Uploading files
x
<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 optional max); 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; null renders the indeterminate state
  • min — number (default 0) — start of the range
  • max — number (default 100) — end of the range
  • format — Intl.NumberFormatOptions — how Progress.Value formats the number (percent, bytes, …)
  • locale — Intl.LocalesArgument — locale for formatting; defaults to the runtime locale
  • getAriaValueText — (formattedValue, value) => string — human-readable text for aria-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.Root renders role="progressbar" with aria-valuemin, aria-valuemax, and aria-valuenow (omitted while indeterminate), exposing the value to screen readers.
  • Labelling — a Progress.Label is linked via aria-labelledby. With no visible label, pass aria-label on Progress.Root so the bar isn’t anonymous; getAriaValueText supplies 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 progressbar role 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 Progress for task completion that moves forward over time; use Meter for 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.