Progress

Use a determinate bar whenever you know how much is left — it is the only one that tells the reader anything. Omit value and the bar animates instead, which says "still working" and nothing more.

Determinate

stack({ gap: 'lg' },
  progress({ value: 25 }),
  progress({ value: 60 }),
  progress({ value: 100 }),
)

Indeterminate

progress()

A bar with no label is marked aria-hidden — a progressbar role with no accessible name tells a screen reader nothing, so an unlabelled bar is treated as decoration. Label anything a reader is meant to follow.

Labels

A label names what is happening; showValue adds the percentage on the right.

Rendering pages72%
Optimising images50%
Waiting for the deploy
stack({ gap: 'lg' },
  progress({ value: 72, label: 'Rendering pages', showValue: true }),
  progress({ value: 30, max: 60, label: 'Optimising images', showValue: true }),
  progress({ label: 'Waiting for the deploy' }),
)

Colors and height

Passed80%
Degraded45%
Failing20%
stack({ gap: 'lg' },
  progress({ value: 80, color: 'success', label: 'Passed', showValue: true }),
  progress({ value: 45, color: 'warning', label: 'Degraded', showValue: true }),
  progress({ value: 20, color: 'danger', label: 'Failing', showValue: true }),
  progress({ value: 60, color: 'neutral', height: 'xs' }),
  progress({ value: 60, color: 'primary', height: '1rem' }),
)

A scale other than 100

max lets you pass the raw numbers — pages built out of pages total — instead of working out a percentage first.

118 of 169 pages70%
progress({ value: 118, max: 169, label: '118 of 169 pages', showValue: true })

Spinner

A spinner is sized in em, so it matches whatever text it sits beside without being told a size.

stack({ direction: 'row', gap: 'lg', align: 'center' },
  spinner({ size: 'sm' }),
  spinner(),
  spinner({ size: 'lg' }),
)

Spinner in context

Give a standalone spinner a label so it is announced. One inside a button does not need one — the button already says what it is doing.

Fetching the latest build…

stack({ gap: 'md' },
  stack({ direction: 'row', gap: 'sm', align: 'center' },
    spinner({ label: 'Loading' }),
    text({ variant: 'small', tone: 'muted' }, 'Fetching the latest build…'),
  ),
  stack({ direction: 'row', gap: 'sm' },
    button({ loading: true }, 'Deploying'),
    button({ variant: 'outline', loading: true }, 'Checking links'),
  ),
)

Props

progress() — exported as progressBar too:

PropTypeDefaultDescription
valuenumberHow far along. Omit for the indeterminate animation.
maxnumber100What value counts as complete.
color'primary' | 'neutral' | 'success' | 'warning' | 'danger''primary'Fill colour.
labelChildText above the bar; also its accessible name.
showValuebooleanfalseShow the percentage beside the label.
heightSpace'0.5rem'Bar thickness.

spinner():

PropTypeDefaultDescription
size'sm' | 'md' | 'lg''md'Diameter. Medium is sized in em, to match the text beside it.
labelstringAccessible name. Without one the spinner is hidden from screen readers.