flyingsalmon

Progress

A bar for an operation with a known end. It reports how far along the work is when the fraction is computable, and that the work is running when it is not.

Spinner, skeleton, progress, stepper

Four ways to report that something is happening, and each owns a different subject. A spinner is a control's own busyness: the button the user just clicked is working. A skeleton is a region loading: content whose shape is already known has not arrived yet. A progress bar is an operation with a known end: a long job with nothing to fake and no control to attach to. A stepper is a position: step 2 of 4 in a flow the user is walking, not work a machine is doing.

Progress is domain-blind. It takes a number and a maximum and draws a bar. The app decides what the number means and writes the phase label beside it.

Determinate

Pass value and the fill animates to that fraction of max, which defaults to 100. Each new value retargets the spring from wherever the fill currently sits, so a stream of small steps reads as one continuous travel rather than a series of jumps. The spring never overshoots, because a fill past its value misreports the work.

Light
20%
Dark
20%

Indeterminate

Omit value and the bar goes indeterminate: a segment travels the track on a 2s cycle, the same tempo skeleton pulses on. This is the state for work that has started and will end, but whose fraction is not computable yet — the window before the first progress event arrives. Swap to a number as soon as one exists.

Light
Dark

A custom max

max lets the app count in its own units instead of converting to a percentage first. A generation that writes seven days counts to seven. value clamps to [0, max] for both the fill and the announced value, so an off-by-one from a server never draws a bar past its end.

Light

Day 5 of 7

Dark

Day 5 of 7

The app owns the text

Progress has no text slots. It draws a bar and names itself for a screen reader; the label above it and the value beside it are the app's, because only the app knows what phase the work is in and how to word it. Compose them around the bar.

Light
Building your trip62%

Picking anchors in Kyoto

Dark
Building your trip62%

Picking anchors in Kyoto

States

The bar is not interactive: no hover, no focus, no press, no disabled, and no tab stop. It carries its phase as data-state, which is indeterminate, loading, or complete once value reaches max. A full bar keeps the fill color rather than turning green: the result of the operation belongs to the app, in the place the user is already looking. data-state is the hook for an app that wants to react to the end of the work.

Light
Dark

Color

Two tokens: --progress-track for the groove and --progress-fill, which aliases --indicator, for the fill. The fill clears 3:1 against the track in both modes, so the boundary is readable without color vision. The track sits at 1.26:1 against the page, so an empty bar is visible without a border.

Accessibility

The bar is a progressbar with aria-valuemin at 0 and aria-valuemax at max. When determinate it carries aria-valuenow, and a screen reader reads a percentage. When indeterminate it carries no aria-valuenow at all, so no percentage is announced before one can be computed.

label becomes aria-label and defaults to “Loading”. Name the work whenever more than one bar can be on screen. The bar sets no aria-busy: that belongs on the app's region, which knows which content is waiting.