Loading Indicator

A visual signal that the system is busy — spinner, progress bar, or skeleton — telling the user to wait and roughly how long.

Why it matters

Latency is inevitable; perceived latency is designable. The right indicator at the right threshold makes an app feel responsive, while the wrong one (a spinner that flashes for 80ms, or none for a 4-second load) makes it feel broken. The component standardizes which indicator to use, when to show it, and how it’s announced to assistive tech — decisions every async surface otherwise improvises.

How it works

Match the indicator to what’s known about duration, and gate it on a delay:

  • Spinner / indeterminate — unknown duration, short-ish waits (a button submit, a panel fetch).
  • Progress bar — known/measurable progress (uploads, multi-step jobs); show percent.
  • Skeleton — content placeholders mirroring final layout; best for whole views/lists, prevents layout shift.
  • Delay threshold — only show after ~150–300ms so fast responses don’t flash a spinner.
IndicatorWhenSignal
SpinnerUnknown, short”working”
Progress barKnown progresspercent
SkeletonView/list loadshape preview
Inline (button)Action pendingdisable + spin

Accessibility: convey busy state non-visually — aria-busy, a live region, or role="progressbar" with value — and respect prefers-reduced-motion for spinners.

Example

A dashboard fetch: instead of a centered spinner, the cards render as skeletons matching their real layout, so when data arrives nothing jumps. A submit button uses the inline pattern — label swaps to a spinner and the control disables, blocking double-submit. A file upload shows a determinate progress bar with “63%.” A 90ms refetch shows nothing because it resolved before the 200ms threshold.

Pitfalls

  • Spinner flash — showing then instantly hiding for sub-200ms loads looks janky; gate on a delay.
  • Layout shift on resolve — spinners that get replaced by taller content jump the page; prefer skeletons that reserve space.
  • Silent to screen readers — a purely visual spinner leaves AT users guessing; use aria-busy/live regions. See accessibility.
  • Indeterminate where progress is known — a spinner for a measurable upload hides useful info; use a bar.

See also