Skip to content

ProgressBar

How far along something is, or that it is going at all. Reach for it when the work has a size: bytes uploaded, rows imported, steps of a wizard finished. When it does not, this component still has an answer, and it is the same component with no value; a spinner is the smaller version of that answer for a control that has to sit inside a row of text.

Determinate and indeterminate are one component because they are one control to a screen reader: the same progressbar role, with the value present or absent.

tsx
/**
 * Three bars: one the buttons move, one on a range of its own, and one
 * that cannot say how far along it is.
 *
 * `Upload` is the ordinary case. Its value is a cell between 0 and 1,
 * and the bar reports it as it stands: nothing is clamped on the way
 * to a screen reader, and the fill is clamped only because a fill
 * wider than its track would be a lie about the picture rather than
 * about the number.
 *
 * `Disk used` has `min` and `max` of its own, so the value is 34 out
 * of 120 rather than a fraction someone had to work out first.
 *
 * `Indexing` was built with no value at all, which is what makes it
 * indeterminate: a sliver sweeps the track, and the record says `busy`
 * with no value rather than a value of zero. Zero would be a claim
 * that nothing has happened yet, which is not what "unknown" means.
 * Whether a bar is determinate is decided once, when it is built, so a
 * bar that starts with no value and later gets one has to be built
 * again: give it a `key` that changes with it.
 */
export function Uploads(_inputs: Inputs<{}>, _ctx: ComponentContext) {
  const progress = internalState(0.35);
  const caption = progress.pipe(map(value => `${Math.round(value * 100)}% of 24 MB`));

  return (
    <column gap={16} padding={20} width={percent(100)} height={percent(100)}>
      <column gap={6}>
        <row gap={8}>
          <text text="Upload" fontSize={13} flexGrow={1} />
          <text text={caption} fontSize={12} color="textMuted" />
        </row>
        <ProgressBar label="Upload" value={progress} width={percent(100)} />
      </column>
      <row gap={8}>
        <Step label="Less" onPress={() => (progress.value = Math.max(0, progress.value - 0.15))} />
        <Step label="More" onPress={() => (progress.value = Math.min(1, progress.value + 0.15))} />
      </row>
      <column gap={6}>
        <text text="Disk used" fontSize={13} />
        <ProgressBar label="Disk used" value={34} min={0} max={120} thickness={10} width={percent(100)} />
      </column>
      <column gap={6}>
        <text text="Indexing" fontSize={13} />
        <ProgressBar label="Indexing" width={percent(100)} />
      </column>
    </column>
  );
}

/** A button, hovering and pressing as everything clickable here does. */
function Step(inputs: Inputs<{ label: string; onPress: () => void }>) {
  return (
    <button
      label={inputs.label.value}
      onClick={inputs.onPress.value}
      paddingLeft={12}
      paddingRight={12}
      paddingTop={6}
      paddingBottom={6}
      borderRadius={6}
      backgroundColor="controlBackground"
      cursor="pointer"
      modifiers={[HOVER_CONTROL]}>
      <text text={inputs.label} fontSize={12} color="controlForeground" />
    </button>
  );
}

Props

PropTypeDefaultWhat it does
valuenumbernoneThe work done so far. Omitting it makes the bar indeterminate.
minnumber0The bottom of the range the value is measured against.
maxnumber1The top of it.
labelstring'Progress'What a screen reader reads.
thicknessnumber6The height of the track, and the radius of both its ends.

The bar has no width of its own: it fills what it is given, so pass a width or a flex. The rest of the layout props on the library page apply.

rootModifiers reaches the track, which is the element the bar is, so a sharedElement or a motion can be put on one without wrapping it in a box of its own.

There are no colour props. The track is controlBackground and the fill is controlAccent, so a theme restyles every bar at once.

The value, and the range

value, min and max are all bindings, and what a screen reader is told is the value as it stands rather than a fraction worked out from it. Disk used in the example reports 34 of 120, which is what the number means; a bar that reported 0.283 instead would have thrown away the units on the way.

The fill is clamped to the track, because a fill wider than its track would be a lie about the picture. The reported value is not clamped, because that would be a lie about the number.

Whether a bar is determinate is decided once, when it is built, from whether value was supplied. A bar that starts with no value and later gets one does not change its mind: give it a key that changes with the answer, so the old one leaves and a determinate one arrives.

Indeterminate

Given no value, a sliver sweeps the track and the record says busy with no value at all. It sweeps in twenty-four positions 90 ms apart, which is the same shape the spinner uses and costs the same: one property written eleven times a second rather than one written on every frame. The sliver's position is a percentage of the track, so the sweep follows the bar when the bar is resized, and the spec beside the example measures both of those.

It keeps sweeping under reduced motion, for the reason a spinner keeps turning: a still indeterminate bar states that nothing is happening.

Keyboard

None. A progress bar reports and does not take input. A control that lets the reader choose a value in a range is a slider, which looks similar and is a different thing entirely.

Semantics

WhatValue
Roleprogressbar, on the track
Namelabel, which is Progress when none was given
Valuevalue, min and max while determinate; none of the three otherwise
Statesbusy while indeterminate, and nothing while determinate

An indeterminate bar omits its value rather than reporting zero. That is ARIA's rule and it is the right one: zero is a stronger and different claim than unknown, and a reader told "0 per cent" hears that nothing has happened yet.

The value arrives as it changes rather than being read once, so a bar the application moves from elsewhere updates what an assistive technology sees without anything re-rendering.

Next

Spinner is the small indeterminate one, and Slider is what to reach for when the reader is choosing the value rather than watching it.