Live demo: loading-states-demo.html (BEFORE/AFTER)
What breaks in many loading spinners
- Visual-only feedback: a spinner with no text (screen readers get nothing).
- No busy state: the UI updates, but there’s no
aria-busysignal. - Double-submits: the same action can be triggered repeatedly during loading.
- Spinner forever: no timeout, no error message, no retry path.
- Motion overload: shimmer/spin animations ignore
prefers-reduced-motion.
Minimum viable pattern
- Provide a text label for the loading state (e.g., “Loading results…”).
- Disable the triggering control while it’s in flight (or debounce), and restore it on completion.
- Mark the updating region
aria-busy="true"while content is changing. - Announce meaningful transitions (start + complete) via
role="status"(polite). - Respect
prefers-reduced-motion(reduce or remove continuous animation).
When to use which semantics
role="status": for short, non-urgent updates (“Loading…”, “Saved”).aria-busy: to mark a region as updating; useful when content changes under a heading/list.role="progressbar": for determinate progress (you know %). Avoid fake precision.- Skeletons: can reduce layout shift, but still need a text status for non-visual users.
A practical checklist
- Don’t move focus just because something is loading.
- Don’t spam: avoid announcing every keystroke or every tiny streaming chunk.
- Handle failure: show an error message with retry, and stop the spinner.
- Keep it bounded: timeouts, cancellation, or “Try again” if the request stalls.