Skip to content

Progress indicators

components specs/components/progress-indicators.kmd

Visual feedback that an operation is in progress — linear (bar) or circular (spinner), determinate (known %) or indeterminate (unknown duration). Material parity (`/components/progress-indicators`).

When this spec applies

Primary triggers

All triggers

Specification body

Spec — Progress indicators

Facet Visual of Koder Design. Material parity: https://m3.material.io/components/progress-indicators.

2 shapes × 2 modes

Shape \ ModeDeterminateIndeterminate
LinearBar fills 0 → 100%Sliding band animates left → right (no fill)
CircularArc sweeps 0 → 360°Arc rotates continuously

Pick:

  • Shape: linear when there's horizontal real estate (above content, banner-like); circular when space is constrained (button, dialog, icon-sized).
  • Mode: determinate when % is known (file upload); indeterminate when not (network handshake, ML inference).

Anatomy (linear, determinate)

   45% complete
   ████████░░░░░░░░░░░░░░░░░░░░░
   ←──────── 100% ────────────→
  • Track height: 4 px (default) / 8 px (large)
  • Track bg: surface-container-highest (or primary 12%)
  • Active fill: primary color
  • Corner radius: track height / 2 (pill shape)
  • Stop point (Material 3): 4 px circle of primary color at the end of the track to indicate "track end"

Anatomy (circular):

       ╭ ━━━╮
       ┃    ╲
       ┃     ●    ← stroke head
       ╲    ╱
        ╲ ━╱
  • Diameter: 24 px (small) / 32 px (default) / 48 px (large)
  • Stroke width: 4 px (default), scales with size
  • Track: surface-container-highest ring
  • Active arc: primary color

R1 — Linear determinate

  • Fill animates from current % to new % (motion-fast, ~150 ms)
  • Don't reset to 0 between updates — smooth interpolation
  • At 100%: brief settle (stop-point disappears as track ends)
  • Backwards progress (resume after pause) allowed — animate back

R2 — Linear indeterminate

  • Sliding band: 30-50% of track width, slides left → right continuously
  • Cycle duration: 2 s
  • Easing: cubic ease-in-out
  • No stop point in indeterminate mode

R3 — Circular determinate

  • Arc length proportional to %
  • Starts at 12 o'clock; sweeps clockwise
  • Stroke ends with rounded cap

R4 — Circular indeterminate

  • Full circle rotates clockwise: 1.5 s per rotation
  • Arc length pulses: 25% → 75% → 25% over 1 cycle
  • Two pulses per rotation (Material 3 default; document if changed)

R5 — Buffer indicator (linear only)

For media / download with "buffered ahead of playhead":

   ████████████░░░░░░░░░░░░░░░░░░░
   ━━━━━━━━━━━━━━━━━░░░░░░░░░░░░░░
   ↑               ↑
   playhead (filled)
                   buffered (lighter)
  • Buffer color: primary 40% (lighter than active)
  • Buffer always ≥ playhead position

R6 — Indicator placement

PlacementUse
Above content (full-width linear)Page-level loading
In-button (small circular replaces text)Submitting a form
Inline next to label (24 px circular)Item-level operation (e.g., row syncing)
Centered in container (large circular)Empty card / pane loading
In app bar bottom edge (linear)Long page operations (refresh)

R7 — Combine with action button

When a button triggers a long operation, replace its content with a small circular indicator (24 px):

  Before: [   Submit   ]
  During: [    ◐       ]   ← circular indeterminate, 24 px
  After:  [✓  Saved   ]
  • Button width stays the same (avoid layout shift)
  • Disable the button during operation
  • On success: brief check icon (1 s) before reverting to idle / new label

R8 — Multi-step / segmented (linear)

Linear can show discrete steps:

   ███ ███ ███ ░░░ ░░░    Step 3 of 5
   step1 step2 step3 step4 step5
  • 2 px gap between segments
  • Each segment radius = track height / 2
  • Filled segments use active color; unfilled use track color

Used for: onboarding stepper, multi-page form indicator.

R9 — Empty / not-started state

Don't show the indicator at 0% — looks broken. Either:

  • Show indicator only after operation starts (delay 200-400 ms)
  • Begin animation as soon as visible (indeterminate)

For determinate, if value is genuinely 0, render the track only (no fill) and show "0%" or "Not started" label nearby.

R10 — Completion state

Determinate at 100%:

  • Brief settle (200 ms)
  • Fade out indicator OR replace with success icon (✓)
  • For background operations, fade out after 1-2 s

R11 — Accessibility

  • Determinate linear / circular: role="progressbar" + aria-valuenow / aria-valuemin / aria-valuemax + aria-label describing what's loading
  • Indeterminate: role="progressbar" + aria-busy="true" + no aria-valuenow (omitted means indeterminate per ARIA)
  • For long operations, periodically announce progress to screen reader (e.g., every 25%): aria-live="polite" on container
  • Don't rely on color alone — pair with text label ("45% complete") for determinate

R12 — Animation

  • Determinate update: motion-fast (~150 ms) interpolation
  • Indeterminate linear cycle: 2 s
  • Indeterminate circular cycle: 1.5 s
  • All animations respect prefers-reduced-motion:
    • Linear indeterminate: pulsing opacity 50% → 100% instead of sliding
    • Circular indeterminate: no rotation; opacity pulse instead

R13 — States

Progress indicators don't have hover / pressed / focused states (they're decorative output, not interactive).

Disabled state IS valid — operation cancelled:

  • Indicator fades to 38% opacity
  • Active animation stops
  • Track stays visible briefly before fading entirely

R14 — Density

DensityLinear heightCircular default
Compact2 px20 px
Default4 px32 px
Comfortable8 px48 px

R15 — Per-preset variation

PresetLinearCircular
material3Track + stop point at endStroke with rounded cap, 2 pulses/rotation
material2No stop point; flat endsFull rotation, 1 pulse
ios_cupertinoSolid bar without stopUIActivityIndicator gear-style
gnomeGtkProgressBar styleAdwaita spinner (3 dots)
windows_11Accent bar with subtle glowFluent spinner (dots circling)
brutalistSolid block, no easing (step changes only)Square instead of circle, hard rotation
terminal_classic[██████░░░░] 60% ASCIISpinner cycle `

R16 — Forbidden patterns

  • ❌ Indeterminate when % is actually known (defeats user trust)
  • ❌ Multiple progress indicators stacked on the same operation
  • ❌ Indicator < 24 px for circular (unreadable)
  • ❌ Animation that resets to 0 between % updates
  • ❌ Showing indicator for < 200 ms (annoying flash; either delay it or do without)
  • ❌ Operation that runs > 30 s with no progress feedback (always show indicator)
  • ❌ Indicator without label / context (user can't tell WHAT is loading)
  • ❌ Hiding the button label entirely on submit without a replacement (no feedback that work started)
  • themes/motion.kmd — easing for indeterminate cycle
  • themes/color-roles.kmdprimary for active, surface-container-highest for track
  • interaction/states.kmd — disabled overlay
  • components/buttons.kmd — in-button progress pattern
  • foundations/elements.kmd — Output family

Requirements (testable)

Requirement: Determinate progress interpolates smoothly {#req-progress-determinate-interpolation}

A determinate indicator SHALL animate its fill from the current value to each new value using a motion-fast (~150 ms) interpolation, and SHALL NOT reset to 0 between updates; backwards progress (resume after pause) SHALL animate back to the lower value rather than snapping.

Scenario: Forward update interpolates from current value

  • GIVEN a linear determinate indicator currently rendering 45%
  • WHEN its value is updated to 60%
  • THEN the fill animates from 45% to 60% over roughly 150 ms
  • AND the fill never returns to 0% during the transition

Scenario: Resume after pause animates backwards

  • GIVEN a determinate indicator rendering 70%
  • WHEN its value is updated to a lower value of 55% (operation resumed at an earlier point)
  • THEN the fill animates back down to 55%
  • AND the change is a smooth interpolation, not an instant snap

Requirement: Progress indicators expose ARIA progressbar semantics {#req-progress-a11y-determinate}

A determinate linear or circular indicator SHALL expose role="progressbar" with aria-valuenow, aria-valuemin, and aria-valuemax set, plus an aria-label describing what is loading, and SHALL pair the value with a visible text label (e.g. "45% complete") so meaning is not conveyed by color alone; an indeterminate indicator SHALL expose role="progressbar" with aria-busy="true", SHALL NOT set aria-valuenow, and SHALL carry aria-live="polite" on its container so long-running progress can be announced.

Scenario: Determinate indicator carries value and label

  • GIVEN a linear determinate indicator loading a file upload at 45%
  • WHEN the indicator is rendered
  • THEN it has role="progressbar" with aria-valuenow="45", aria-valuemin="0", and aria-valuemax="100"
  • AND it has a non-empty aria-label describing the operation
  • AND a visible text label reading "45% complete" is present

Scenario: Indeterminate indicator omits aria-valuenow

  • GIVEN an indeterminate indicator for a network handshake of unknown duration
  • WHEN the indicator is rendered
  • THEN it has role="progressbar" and aria-busy="true"
  • AND it has no aria-valuenow attribute
  • AND its container carries aria-live="polite"

Requirement: In-button progress preserves layout and disables the button {#req-progress-in-button}

When a button triggers a long operation and its content is replaced by a small (24 px) circular indeterminate indicator, the button width SHALL stay unchanged to avoid layout shift, the button SHALL be disabled for the duration, and on success a brief check icon SHALL be shown before reverting to an idle or new label; the label SHALL NOT be hidden without a replacement indicator.

Scenario: Submitting swaps label for indicator without resizing

  • GIVEN a "Submit" button at a fixed rendered width
  • WHEN the user triggers the long operation
  • THEN the button content is replaced by a 24 px circular indeterminate indicator
  • AND the button width is identical to its pre-submit width
  • AND the button is disabled

Scenario: Success shows check before reverting

  • GIVEN a button showing the in-button indicator during an operation
  • WHEN the operation completes successfully
  • THEN a check icon is shown briefly (about 1 s)
  • AND the button then reverts to an idle or new label and becomes enabled again

Requirement: Indeterminate animation honors reduced motion {#req-progress-reduced-motion}

Indeterminate indicators SHALL animate continuously by default (linear sliding band on a 2 s cycle, circular rotation on a 1.5 s cycle), but when prefers-reduced-motion is set they SHALL replace translational/rotational motion with an opacity pulse (50% → 100%) and SHALL NOT slide or rotate.

Scenario: Default indeterminate linear slides

  • GIVEN a linear indeterminate indicator and no reduced-motion preference
  • WHEN it animates
  • THEN a sliding band of 30–50% track width moves left → right on a 2 s cycle
  • AND no stop point is rendered

Scenario: Reduced motion replaces rotation with opacity pulse

  • GIVEN a circular indeterminate indicator
  • WHEN prefers-reduced-motion is set
  • THEN the indicator does not rotate
  • AND it animates an opacity pulse between 50% and 100% instead

Requirement: Forbidden progress patterns are not emitted {#req-progress-forbidden-patterns}

An implementation SHALL NOT use an indeterminate indicator when the percentage is actually known, SHALL NOT render a circular indicator smaller than 24 px, SHALL NOT stack multiple indicators on the same operation, and SHALL NOT flash an indicator for under 200 ms (either delay it or omit it).

Scenario: Known progress uses determinate mode

  • GIVEN an operation whose completion percentage is known (a file upload)
  • WHEN an indicator is chosen for it
  • THEN a determinate indicator is used
  • AND the indicator is not indeterminate

Scenario: Sub-threshold operation does not flash an indicator

  • GIVEN an operation expected to finish in under 200 ms
  • WHEN the indicator would be shown
  • THEN its appearance is delayed (200–400 ms) so a completing operation shows nothing
  • AND no indicator flashes for under 200 ms

References