Progress

Waiting is an experience we design. Tell the user something's happening, and if possible, how long it will take. Never leave them guessing.

Determinate

Linear bar

For processes with a known duration — uploads, imports, broker scans. The fill animates smoothly from its last value, never jumps.

Default · 68%
Accent · 40%
Thin · inline · 88%
Thick · hero · 25%
Indeterminate

When you don't know how long

For short waits where a percentage would be misleading. Under 1 second, prefer nothing at all — the motion is often more distracting than the wait.

Linear indeterminate
Spinner · small
Spinner · default
Spinner · large accent
Circular

Progress ring

For compact surfaces — dashboard tiles, mobile list rows. Pair with a numeric value; the ring alone is too precise to read at a glance.

25%
70%
100%
Stepper

Multi-step flows

For onboarding and checkout. Each segment is one step; accent represents completed, muted represents upcoming.

Step 2 of 4
2 / 4
Final step
4 / 4
Skeleton

For content that's loading

Use when the layout is stable but the content is arriving. The skeleton matches the final content's shape — never generic grey rectangles in the wrong size.

Rules

Picking the right one

Known duration → determinate.

If the backend tells you percentage, always show it. Even coarse estimates are better than a spinner.

Sub-second wait → nothing.

Flashing a spinner for 300ms reads as a bug. Debounce loaders to appear only after 500ms of delay.

Layout will shift → skeleton.

If the final content will push things around, show the skeleton so users can orient before data lands.

Never stack loaders.

A page spinner plus three row spinners plus a skeleton is chaos. One load state per surface.