Progress indicators

mandatory

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

Spec — Progress indicators

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

2 shapes × 2 modes

Shape Mode Determinate Indeterminate
*inear* Bar fills 0 → 100% Sliding band animates left → right (no fill)
*ircular* Arc sweeps 0 → 360° Arc rotates continuously

Pick:

  • *hape* linear when there's horizontal real estate (above content,

    banner-like); circular when space is constrained (button, dialog, icon-sized).

  • *ode* determinate when % is known (file upload); indeterminate

    when not (network handshake, ML inference).

Anatomy (linear, determinate)

   45% complete
   ████████░░░░░░░░░░░░░░░░░░░░░
   ←──────── 100% ────────────→
  • *rack height* 4 px (default) / 8 px (large)
  • *rack bg* surface-container-highest (or primary 12%)
  • *ctive fill* primary color
  • *orner radius* track height / 2 (pill shape)
  • *top point*(Material 3): 4 px circle of primary color at the

    end of the track to indicate "track end"

Anatomy (circular):

       ╭ ━━━╮
       ┃    ╲
       ┃     ●    ← stroke head
       ╲    ╱
        ╲ ━╱
  • *iameter* 24 px (small) / 32 px (default) / 48 px (large)
  • *troke width* 4 px (default), scales with size
  • *rack* surface-container-highest ring
  • *ctive 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 easeinout
  • 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

Placement Use
*bove content*(full-width linear) Page-level loading
*n-button*(small circular replaces text) Submitting a form
*nline next to label*(24 px circular) Item-level operation (e.g., row syncing)
*entered in container*(large circular) Empty card / pane loading
*n 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

Density Linear height Circular default
Compact 2 px 20 px
Default 4 px 32 px
Comfortable 8 px 48 px

R15 — Per-preset variation

Preset Linear Circular
material3 Track + stop point at end Stroke with rounded cap, 2 pulses/rotation
material2 No stop point; flat ends Full rotation, 1 pulse
ios_cupertino Solid bar without stop UIActivityIndicator gear-style
gnome GtkProgressBar style Adwaita spinner (3 dots)
windows_11 Accent bar with subtle glow Fluent spinner (dots circling)
brutalist Solid block, no easing (step changes only) Square instead of circle, hard rotation
terminal_classic [██████░░░░] 60% ASCII Spinner 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

Source: ../home/koder/dev/koder/meta/docs/stack/specs/components/progress-indicators.kmd