Overview
Progress bar lives in @flatpay-dk/ui. Pass a numeric value for a determinate fill, or value={null} to render the indeterminate stripe. The fill is translucent black so the bar adopts whatever surface it sits on — no theming required.
Use a label, every time
Pair the bar with copy that names what's progressing — a filename, a step count, a job name. The bar is reinforcement; the label is the content. Pass an aria-label (or aria-labelledby) so screen readers carry the same information.
Sizes
Two heights. sm (4 px) sits inline next to content; md (16 px) carries a single-task surface where progress is the focus.
size — sm
4 px tall. For inline progress in dense rows — a cell in a table, a row in a list. Quiet enough to sit beside content.
size — md
16 px tall. The headline indicator on a single-task surface — file upload, onboarding step, batch import.
Indeterminate
Pass value={null} when the duration is unknown — third-party syncs, ingestion jobs, webhook acks. A 35 %-wide stripe sweeps left to right on a 1.4 s loop. Reaches the right edge, then loops; never bounces.
Honour prefers-reduced-motion: users who've opted out see the stripe held still in the centre as a visible “still working” mark.
In context
Where progress bars earn their place — a single task with a measurable end. If you can't name what 100 % means, you don't want a bar.
File upload
Onboarding
Indeterminate
Use indeterminate when total duration is unknown — webhook acks, ingestion jobs, third-party syncs. Once you can estimate, switch to a value.
Anatomy
The bar is two layers: a track and a value fill.
Track
The full-width container. 5 % black so it picks up whatever surface it sits on. Rounded — 2 px on `sm`, 8 px on `md`.
Value
The filled portion. 28 % black, same surface adoption. Width is driven by `value / max`; transitions via transform-scale on the X axis so the bar never animates `width`.
Behavior
Watch the value advance and the bar follow. Width changes use a 300 ms cubic-bezier(0.16, 1, 0.3, 1) ease — the same curve as the rest of the system's entrances. Indeterminate mode runs at 1.4 s per loop.
Accessibility
The bar always reports role="progressbar". Determinate bars expose aria-valuenow / min / max; indeterminate bars omit aria-valuenowso assistive tech announces “busy” instead of a misleading number.
Animation respects user preference
The indeterminate stripe halts under prefers-reduced-motion: reduce; the determinate transition collapses to instant. The bar still communicates state — just statically.
Code
Determinate
tsx
import { ProgressBar } from "@flatpay-dk/ui";
<ProgressBar
size="md"
value={73}
aria-label="Upload progress, 73 percent"
/>;Indeterminate
tsx
<ProgressBar
size="md"
value={null}
aria-label="Syncing from GitHub"
/>;Custom max
tsx
// Total bytes uploaded out of total bytes — no need to convert to percent.
<ProgressBar
size="sm"
value={2_437_120}
max={4_298_753}
aria-label="Upload progress"
/>;Best practices
A progress bar earns its place when there is exactly one task with a measurable end. Anything else is decoration.
Do
Pair the bar with the thing it's measuring — filename, step count, job name. The bar reinforces; the label explains.
Don't
Don't ship a stand-alone bar with no label. The user reads the colour, not what's progressing.
Do
Use indeterminate when the end is unknown. Switch to a value as soon as one is available.
Don't
Don't fake progress. A bar that crawls to 95 % and stalls erodes trust faster than no bar at all.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value | number | null | null | Numeric progress in [0, max]. Pass null for indeterminate. |
| max | number | 100 | Upper bound of the value range. |
| size | 'sm' | 'md' | 'md' | 4 px or 16 px tall. |
| onFill | boolean | false | Set when placing the bar on a coloured surface. Reserved for a future token swap; today the visual matches the default. |
| aria-label | string | — | Accessible name. Required unless a labelledby relationship is wired externally. |
| className | string | — | Forwarded to the wrapper. |