ProgressBar
Shows how far a task has come — an upload, a multi-step import, a page load — as a labelled bar with a known or unknown end. Meter's sibling for things that are happening rather than things that are measured.
Basic usage
Install the package:
pnpm add @design-schema/react @design-schema/tokens Import the stylesheet once for the app, then the component:
import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { ProgressBar } from '@design-schema/react'; Examples
import { ProgressBar } from '@design-schema/react';
<ProgressBar label="Uploading photos" value={42} />Props
9 rows| Prop | Type | Default | Description |
|---|---|---|---|
| label required | string | — | What is progressing ("Uploading photos", "Importing contacts"). Visible unless `hideLabel`. Accessibility: The accessible name: aria-labelledby on web, aria-label on the Lit host (ids do not cross the shadow root), accessibilityLabel on React Native. An empty label leaves the bar unnamed, with no development warning. |
| value | number | — | Progress so far, between `min` and `max`. Omit (undefined or null) for an indeterminate bar (the end is unknown); generated code types it `number | null | undefined`. Clamped to `min`…`max` for the fill, the accessible value, `formatValue`'s argument and the announcement tiers; a non-finite number (NaN, Infinity) is treated as `min`. An unparseable Lit `value` attribute (`value="abc"`) parses to NaN and is therefore determinate at `min`: only a removed or absent attribute, or an explicit null/undefined property, is indeterminate. |
| min | number | 0 | Start of the range. A non-finite number is treated as the default, 0. |
| max | number | 100 | End of the range. A non-finite number is treated as the default, 100. |
| formatValue | function (value: number, min: number, max: number) => string | — | Renders the value text ("42%", "3 of 12 files"). Defaults to a percentage over the whole range — `(value − min) / (max − min)` — the same arithmetic the fill uses, so a non-zero `min` reads correctly without a custom formatter, rounded to a whole number: `Intl.NumberFormat(undefined, { style: 'percent', maximumFractionDigits: 0 })` (the runtime or device default locale; there is no locale prop), as Meter does (99.5% of the way shows "100%" before completion; completion is only the clamped value reaching `max`). Called with the clamped value, and not called at all while indeterminate — nothing shows or announces a value then. Rounding is for the text only; the fill uses the exact fraction. A `max` at or below `min` is not a range: the bar renders empty, exposes aria-valuenow / accessibilityValue.now = `min` with the bounds exactly as given (never swapped or clamped, even when that means aria-valuemin is greater than aria-valuemax), exposes "0%" as aria-valuetext and shows it when `showValue` is true, makes no progress or completion announcements, and warns in development once per distinct min/max pair (not once per render or per mount). |
| showValue | boolean | true | Show the value text at the end of the label row. Ignored when indeterminate. Lit attribute is the negated boolean `hide-value` (reflected), since an attribute can only turn things on. |
| hideLabel | boolean | false | Visually hide the label (it remains the accessible name). For bars inside a Card whose heading already says what is happening. The value text, when shown, stays at the inline end of the row (the header switches to end alignment, since the hidden label leaves the flow). When there is no visible value text either, the label row takes no space and `partGap` is not applied: on web and Lit the header element stays, with its data-part and the label inside it, and is itself visually hidden (out of flow), so web's aria-labelledby still resolves. On React Native a hidden label is not rendered at all; the name lives in accessibilityLabel and the `label` part has no native home while hidden, as Input — and with `showValue: false` as well the header View is not rendered either, so `testID="ProgressBar.header"` is absent in that one configuration. On web and Lit the visually-hidden styles go on a wrapper span inside the header, never on the `label` part itself, so the part stays a plain Text with no layout styles of its own. |
| tone | neutral | success | danger | neutral | Neutral while running; `success` at completion, `danger` when the task failed part-way. Paired with a text status elsewhere: the color is never the only signal. |
| announce | none | milestones | complete | complete | What a screen reader hears without focusing the bar: nothing, every 25%, or only completion. Each announcement uses `copy.progress` / `copy.complete`, and `copy.indeterminate` is announced once each time the bar enters the indeterminate state. A value that moves backward resets the tiers already announced, so a retried task announces its progress again on the way up. The full announcement rules are under Behavior. |
Accessibility
Decisions this component leaves to you
Supply an accessible name. This component has no text of its own to fall back on, so without one a screen reader announces its role and nothing else.
Required props that carry accessibility meaning: label. Each is described in the props table above.
Implicit ARIA role: progressbar.
Anatomy
What generation guarantees
Accessible name — Every instance carries a programmatic name. Where the component has no text of its own to use, the name is a prop the consumer supplies.
Contrast — Text and meaningful non-text contrast meets WCAG AA; the pairs are listed below.
Live region — Changes are announced through a live region, without moving focus.
Reduced motion — Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.
Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.