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.
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.
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.
Day 5 of 7
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.
Picking anchors in Kyoto
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.
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.