Skip to content

Progress

Progress shows that work is under way and how far it has come. Use the bar when you know the size of the work, and the spinner for a short wait inside the button or row that started it.

Example

Importing candidates41%
Welders from the Kathmandu intake1,240 of 3,000 candidates

When to use it

Use the bar for work of a known size, such as importing candidates or uploading a folder of passports. Show it where the work started, and let one bar cover every step, so it only moves forward.

Use the spinner for a short wait inside the button or row that started it, such as sending an offer. A page or a panel that is loading shows its skeleton instead.

Anatomy

The bar sits between a label row and a status row. The label names the work, with the percentage on its right. The status row says what is happening and gives the counts.

The spinner is an open circle in the ink of the button or row that holds it.

Bar height
4 px
Bar corners
Fully round
--radius-full
Track
The fill of a control
--surface-control
Fill
Brand blue
--brand-blue
Label
Caption, 12 px, medium
--type-product-caption
Value and status
Caption small, 11 px, tabular digits
--type-product-caption-small
Space between rows
6 px
Fill moves over
500 ms
--ease-standard
Spinner in a control or row
16 px
Spinner in a panel or list footer
20 px
Spinner speed
One turn per second

When the size is unknown

When work starts before its size is known, a third of the track pulses at its start, blue in the middle and fading at both ends, and the status says in words what is happening. Once the size is known, the fill takes its true value and moves forward from there. The bar does not turn into a spinner, or back.

Counting candidates
380 counted so far

Behavior

A button is busy the moment you press it, so the work cannot start twice. Its spinner appears after 300 ms, so a quick save does not flicker.

When stopping loses nothing, offer Cancel on the label row. When stopping would lose work already done, ask before you stop.

When the bar has not moved for 10 seconds, the status row says in words what the work is waiting for.

When work fails, the fill stays where it stopped and the status row says in red what went wrong and what to do: “12 passports did not upload. Try again.”

With reduced motion the bar steps to each value, the spinner fades in and out, and an unknown size shows the track and the words alone.

Content

Name the work with a verb ending in ing and its object: Importing candidates, Uploading passport.pdf. Leave out the ellipsis. The moving bar already says the work goes on.

Give numbers the reader can check, such as 1,240 of 3,000 candidates, and show a percentage only beside them. Leave out time estimates unless the work runs at a steady pace.

Accessibility

The bar is a progress bar named by its label, with a value from 0 to 100. While the size is unknown it has no value, so a screen reader does not announce zero percent.

The bar does not announce each change. The end of the work is announced once, by the toast or the notification that reports it.

The spinner is hidden from screen readers. The button that holds it is marked busy and keeps its name.

Permitted and not permitted

Permitted

  • Importing candidates, 1,240 of 3,000

    Numbers the reader can check.

Not permitted

  • Loading

    Names neither the work nor how much is left.

  • A spinner over the whole page

    A skeleton shows what is coming and leaves the rest of the page usable.

  • A bar that starts again at each step

    When a bar moves backward, readers stop trusting it.

NextSkeleton

Progress

Shows that work is under way: a determinate bar for work of known size, a spinner for a short wait inside the control that started it.

  • Kind: component
  • Page: https://design.werklist.com/components/progress
  • Version: 2.1.2

When to use it

  • Import, upload or sync of known size, such as importing candidates or uploading passports: bar where the work started
  • A button's action takes a moment, such as Send offer: the button is busy at once and its spinner shows after 300 ms (--duration-slow)
  • More rows load at the end of a list: 20 px spinner in the footer

When not to use it

  • A page or panel loading content: Skeleton
  • Long work with no size and no way to learn it: say it in words
  • More than one indicator for one action
  • A blocking overlay with a spinner

Anatomy

  • bar: 4 px high; fully round (--radius-full)
  • track: the fill of a control (--surface-control)
  • fill: brand blue, at least 3:1 against the track in both themes (--brand-blue)
  • label row: label in Caption, 12 px, medium, --text, on the left; value in Caption small, 11 px, tabular, --text-muted, on the right (--type-product-caption, --type-product-caption-small, --text, --text-muted)
  • status row: what is happening on the left, counts on the right, Caption small, 11 px, --text-muted, tabular (--type-product-caption-small, --text-muted)
  • rhythm: 6 px between label row, bar and status row
  • cancel: a small quiet button, Cancel, at the right of the label row, only when stopping loses nothing
  • indeterminate segment: a third of the track at its start, a --brand-blue gradient that is full in the middle and 30 percent at both ends; pulses from 100 to 50 percent opacity over 2,000 ms, cubic-bezier(0.4, 0, 0.6, 1); hidden under reduced motion
  • spinner: an open circle with round ends, in the ink of the control that holds it; one turn per second at a steady speed (--text-muted, --on-brand)

Variants

  • determinate bar: known size
  • indeterminate bar: the start of a bar whose size is unknown
  • spinner: a short wait inside the control, row or list footer that started it

Sizes

  • spinner in controls and rows: height 16 px
  • spinner in panels and list footers: height 20 px

States

  • advancing: fill width animates to the new value over 500 ms (--ease-standard)
  • size becomes known: the segment gives way to the fill at the true value
  • stalled: after 10 seconds without movement the status says in words what the work waits for
  • failed: status row replaced by the failure and what to do, in --error-ink; fill stays (--error-ink)
  • done: the bar leaves; a toast or a notification reports the end
  • spinner delay: the control is busy at once; the spinner appears after 300 ms (--duration-slow)
  • reduced motion: the bar steps to each value; the spinner stays still and pulses from 100 to 50 percent opacity over 2,000 ms; the moving segment is hidden

Keyboard

  • Tab: reaches Cancel when present; the bar and the spinner take no focus

Accessibility

  • bar: role progressbar, aria-label from the label, aria-valuemin 0, aria-valuemax 100, aria-valuenow when known and absent when unknown
  • no live region on the bar; the end is announced once by the toast or notification that reports it
  • spinner: aria-hidden; its host carries aria-busy true and keeps its name
  • fill against track at least 3:1 in both themes

Content

  • label: a verb ending in ing and its object, no ellipsis, no period
  • status: numbers the reader can check, formatted in the app's locale; a percentage only beside them
  • no time estimate unless the work is steady

Tokens

  • --type-product-caption
  • --type-product-caption-small
  • --surface-control
  • --brand-blue
  • --radius-full
  • --text
  • --text-muted
  • --error-ink
  • --on-brand
  • --duration-slow
  • --ease-standard

JSON

{
  "title": "Progress",
  "path": "/components/progress",
  "url": "https://design.werklist.com/components/progress",
  "text": "https://design.werklist.com/agent/components/progress",
  "summary": "A bar for work you can measure, and a spinner for a short wait inside the control that started it.",
  "version": "2.1.2",
  "kind": "component",
  "name": "Progress",
  "purpose": "Shows that work is under way: a determinate bar for work of known size, a spinner for a short wait inside the control that started it.",
  "use": [
    "Import, upload or sync of known size, such as importing candidates or uploading passports: bar where the work started",
    "A button's action takes a moment, such as Send offer: the button is busy at once and its spinner shows after 300 ms (--duration-slow)",
    "More rows load at the end of a list: 20 px spinner in the footer"
  ],
  "avoid": [
    "A page or panel loading content: Skeleton",
    "Long work with no size and no way to learn it: say it in words",
    "More than one indicator for one action",
    "A blocking overlay with a spinner"
  ],
  "anatomy": [
    {
      "part": "bar",
      "measure": "4 px high; fully round",
      "token": "--radius-full"
    },
    {
      "part": "track",
      "measure": "the fill of a control",
      "token": "--surface-control"
    },
    {
      "part": "fill",
      "measure": "brand blue, at least 3:1 against the track in both themes",
      "token": "--brand-blue"
    },
    {
      "part": "label row",
      "measure": "label in Caption, 12 px, medium, --text, on the left; value in Caption small, 11 px, tabular, --text-muted, on the right",
      "token": "--type-product-caption, --type-product-caption-small, --text, --text-muted"
    },
    {
      "part": "status row",
      "measure": "what is happening on the left, counts on the right, Caption small, 11 px, --text-muted, tabular",
      "token": "--type-product-caption-small, --text-muted"
    },
    {
      "part": "rhythm",
      "measure": "6 px between label row, bar and status row"
    },
    {
      "part": "cancel",
      "measure": "a small quiet button, Cancel, at the right of the label row, only when stopping loses nothing"
    },
    {
      "part": "indeterminate segment",
      "measure": "a third of the track at its start, a --brand-blue gradient that is full in the middle and 30 percent at both ends; pulses from 100 to 50 percent opacity over 2,000 ms, cubic-bezier(0.4, 0, 0.6, 1); hidden under reduced motion"
    },
    {
      "part": "spinner",
      "measure": "an open circle with round ends, in the ink of the control that holds it; one turn per second at a steady speed",
      "token": "--text-muted, --on-brand"
    }
  ],
  "variants": [
    {
      "name": "determinate bar",
      "use": "known size"
    },
    {
      "name": "indeterminate bar",
      "use": "the start of a bar whose size is unknown"
    },
    {
      "name": "spinner",
      "use": "a short wait inside the control, row or list footer that started it"
    }
  ],
  "sizes": [
    {
      "name": "spinner in controls and rows",
      "height": "16 px"
    },
    {
      "name": "spinner in panels and list footers",
      "height": "20 px"
    }
  ],
  "states": [
    {
      "name": "advancing",
      "change": "fill width animates to the new value over 500 ms",
      "tokens": [
        "--ease-standard"
      ]
    },
    {
      "name": "size becomes known",
      "change": "the segment gives way to the fill at the true value"
    },
    {
      "name": "stalled",
      "change": "after 10 seconds without movement the status says in words what the work waits for"
    },
    {
      "name": "failed",
      "change": "status row replaced by the failure and what to do, in --error-ink; fill stays",
      "tokens": [
        "--error-ink"
      ]
    },
    {
      "name": "done",
      "change": "the bar leaves; a toast or a notification reports the end"
    },
    {
      "name": "spinner delay",
      "change": "the control is busy at once; the spinner appears after 300 ms",
      "tokens": [
        "--duration-slow"
      ]
    },
    {
      "name": "reduced motion",
      "change": "the bar steps to each value; the spinner stays still and pulses from 100 to 50 percent opacity over 2,000 ms; the moving segment is hidden"
    }
  ],
  "keys": [
    {
      "key": "Tab",
      "action": "reaches Cancel when present; the bar and the spinner take no focus"
    }
  ],
  "accessibility": [
    "bar: role progressbar, aria-label from the label, aria-valuemin 0, aria-valuemax 100, aria-valuenow when known and absent when unknown",
    "no live region on the bar; the end is announced once by the toast or notification that reports it",
    "spinner: aria-hidden; its host carries aria-busy true and keeps its name",
    "fill against track at least 3:1 in both themes"
  ],
  "content": [
    "label: a verb ending in ing and its object, no ellipsis, no period",
    "status: numbers the reader can check, formatted in the app's locale; a percentage only beside them",
    "no time estimate unless the work is steady"
  ],
  "tokens": [
    "--type-product-caption",
    "--type-product-caption-small",
    "--surface-control",
    "--brand-blue",
    "--radius-full",
    "--text",
    "--text-muted",
    "--error-ink",
    "--on-brand",
    "--duration-slow",
    "--ease-standard"
  ]
}

Raw file: https://design.werklist.com/agent/components/progress