Skip to main content

Progress reports aria-valuenow instead of staying indeterminate (WEB-1381)

Version: 0.9.1 ยท Type: ๐Ÿ› Bug Fix

WEB-1381

Problemโ€‹

Progress rendered a visually correct bar but was permanently announced as an indeterminate progressbar. Whatever value the caller passed, the element carried data-state="indeterminate" and no aria-valuenow, so a screen-reader user heard "progress bar" with no percentage.

Root causeโ€‹

ui/progress.tsx destructured value out of props and used it only for the Indicator's translateX transform. It was never forwarded to ProgressPrimitive.Root, and the {...props} spread could not restore it because value had already been pulled out of the rest object.

Radix derives aria-valuenow, aria-valuetext, data-state and data-value from Root's value; with value === undefined it falls back to null, i.e. indeterminate. The visual stayed correct precisely because it is computed independently of the Radix context โ€” which is why the defect survived review and screenshots.

Changed Filesโ€‹

  • packages/design/src/components/ui/progress.tsx
  • packages/design/src/components/Progress/__tests__/Progress.test.tsx

Changesโ€‹

  1. progress.tsx: pass value to ProgressPrimitive.Root in addition to the Indicator transform.
  2. Progress.test.tsx: assert role, aria-valuenow and aria-valuemin / aria-valuemax for 0 / 42.5 / 100, plus a case pinning that a bar rendered without value still stays indeterminate.

Impactโ€‹

AttributeBeforeAfter
aria-valuenowabsentpresent when value is valid
aria-valuetextabsentpresent (getValueLabel, default rounds to a percentage)
data-statealways indeterminateloading / complete
data-valueabsentthe numeric value
  • Visual output is unchanged. The Indicator transform never depended on the Radix context.
  • role="progressbar" is not new โ€” Radix always rendered it; only the value semantics were missing.
  • Radix now validates value and logs a console.error for a non-number / NaN / < 0 / > max. Rendering is unaffected in that case (Radix falls back to indeterminate while the visual still comes from the component's own transform), so this surfaces pre-existing caller bugs rather than creating new ones.
  • If you rely on data-state="indeterminate" in a selector, note it now only applies to bars rendered without a value.

Labelling the barโ€‹

Two things are worth pairing with this fix on the consumer side:

  • Give the bar an aria-label. A progressbar with no accessible name is announced as an anonymous one.
  • If the percentage you print next to the bar is not simply Math.round(value), override getValueLabel. aria-valuetext takes precedence over aria-valuenow in ARIA, and Radix's default label rounds โ€” so a bar that displays a floored 66% would otherwise be spoken as 67%.