Skip to main content

ProgressBar

betav0.1.0

Determinate linear progress: how far along a known quantity of work is. Replaces MUI `LinearProgress`, used in this determinate form on 12 of the products' 13 render sites — a task's completion, an upload, a lesson score. It draws the rail `Slider` already draws, from the same tokens, so the library has one rail and not two. `value` is a percentage, clamped to 0-100 because real product data overshoots both ends. An accessible name is required, not optional: `role="progressbar"` without one is an axe violation. `tone` colours the bar by status — `success`, `error`, or the brand default — for the one panel that does this in a product, and only ever as a reinforcement of a status the caller already shows in text. Use `Spinner` when the remaining work is unknown.

Variants

empty
0%
partial
37%
nearly done
82%
complete
100%
size="md"
60%
size="lg"
60%
tone="default"
Running
tone="success"
Completed
tone="error"
Failed
value={-20} clamps to 0
value={140} clamps to 100
Indexing files

Examples

<ProgressBar value={42} aria-label="Import progress" />
<ProgressBar value={42} size="lg" aria-label="Import progress" />
<div className="flex items-center gap-3">
  <ProgressBar value={pct} className="flex-1" aria-labelledby="task-name" />
  <span id="task-name">{pct}%</span>
</div>
{/* Status colour is a reinforcement: the caption says it too (RULE-04). */}
<div className="flex items-center gap-3">
  <ProgressBar
    value={pct}
    tone={status === "FAILED" ? "error" : status === "COMPLETED" ? "success" : "default"}
    className="flex-1"
    aria-labelledby="operation-name"
  />
  <span id="operation-name">{statusCaption}</span>
</div>

Props

PropType
tone"default" | "success" | "error" — semantic status of the work, "default" unless set. The set is closed by two filters a tone must both clear: a real product behaviour (openaev's bulk-operations panel paints exactly these three, by bulk_operation_status) and 3:1 fill-against-track in both modes. "warning" has neither and is measured, not forgotten. Colour may never be the sole cue: pair it with the icon and caption the caller already renders (RULE-04).
valuenumber, required — completion as a percentage, 0-100. Clamped, and a non-finite value reads as 0: real product call sites overshoot both ends, and an aria-valuenow outside its bounds is an ARIA violation as well as an overflowing bar. No `max` prop — convert before calling.
size"md" | "lg" — 4px (default) or 10px, both observed in the products.
aria-labelstring — names the task the bar measures. Exactly one of aria-label / aria-labelledby is required at compile time.
aria-labelledbystring — id of the visible text naming the task. Prefer it whenever such text exists beside the bar, which it does on every product site.
classNamestring — merged with the track's own classes. The parent owns the width; `flex-1` is what the product sites use.
refReact.Ref<HTMLDivElement> — forwarded to the track, the element carrying role="progressbar".

Accessibility

2.1 AAWCAG pass

Ratios come from `pnpm check:contrast`, computed from theme.css in both modes, never typed by hand. Threshold 3:1 throughout — the bar is a graphical object under WCAG 1.4.11, not text. Indicator against its own track, per tone. default dark: #42caff on #62636a = 3.17:1 ✅, light: #0015a8 on #95969d = 4.26:1 ✅. success dark: #17ab1f on #082114 = 5.59:1 ✅, light: #117916 on #ccefce = 4.46:1 ✅. error dark: #f14337 on #2e0e13 = 4.71:1 ✅, light: #b8180a on #f3ccc9 = 4.49:1 ✅. TONE TRACKS — the semantic tones do NOT reuse the neutral rail: against it they measure 1.96:1/1.89:1 (success) and 1.59:1/2.25:1 (error), under 3:1. Each takes its own 30%-alpha companion instead, the family Chip fills with. WARNING IS NOT SHIPPED and the absence is measured, not overlooked: #e6700f clears 3:1 against NO track in theme.css in light mode (1.07:1 neutral rail, 2.51:1 own companion, 2.81:1 best candidate anywhere). Full tables and the ways out in ProgressBar.rfc.md Q11/Q12 — a token decision, not a component one. RE-AUDIT NOTE — --color-feedback-neutral-secondary (the track), on the Slider model. This token has already moved once without a consumer's knowledge: PR #76 rebound its LIGHT value from --gray-500 to --gray-400, which put Slider's rail at 2.63:1 against the page and sent it to scripts/contrast-coverage-allowlist.json. Here the dark figure clears the floor by only 0.17, and that margin is a property of the two token values, not of anything this component does. TRIGGER: any theme.css change touching --color-feedback-neutral-secondary or --color-filigran-brand-primary re-opens this pair (AGENTS.md "Token Changes Trigger Re-Audit"). THEN: re-run pnpm check:contrast; under 3:1 the component does NOT ship as-is and the track token returns to design (rfc §9 Q8) — Rule 5's allow-list is closed to new components and is the wrong instrument anyway, this being a real value-encoding boundary rather than a supplemental treatment like Slider's rail-against-page. Track-against-page is for that same reason deliberately not declared here at all. Colour is not the sole indicator (WCAG 1.4.1): the filled length carries the value, and every product site renders the number beside the bar. `tone` does not weaken this — it only repeats a status the caller is already conveying in text and in an icon (RULE-04), it changes nothing in the accessibility tree, and a user who cannot separate the three fills loses no information the bar was carrying. role="progressbar" always has an accessible name — the prop union makes one of aria-label / aria-labelledby mandatory at compile time, which is what axe's aria-progressbar-name rule requires — and aria-valuenow always lies inside the declared 0-100 bounds. Not focusable, not interactive: no focus ring applies.