Patterns

Loading

A loading state answers three questions: did my action register, is the system still working, and how much longer. Choose the treatment by what is loading — not only by how long you think it will take.

When to use

  • When a request has no data to show yet and the wait is long enough for the user to notice.
  • When an action the user triggered is still in progress, so they know it registered.
  • When work has measurable progress, such as an upload, conversion, or bulk action.
  • When a region will be replaced by content whose layout you already know.

When not to use

  • Don’t show a loading state when cached or previous data is available — render what you already have and revalidate behind it.
  • Don’t use a spinner for a region whose layout you already know — use Skeleton instead.
  • Don’t use a skeleton for content you cannot draw in advance, such as a chart or a computed total — use LoadingSpinner instead.
  • Don’t use a skeleton when refreshing content the user is already reading — update the content in place.
  • Don’t use an indeterminate spinner for work you can measure — use LoadingSpinner instead, with a progress value.
  • Don’t use a loading state for a successful but empty result — use Empty instead.
  • Don’t leave a skeleton or spinner running after a request fails — use Banner instead, with a retry action.
  • Don’t show a loading state for background work the user didn’t start — notify the user when it finishes.

How to use

Choosing a treatment

Pick by what is loading, because that is the thing you always know up front.

What is loading Use
Cached, previous, or predictable data Nothing — render it and refresh behind it
A view or region whose layout you know in advance Skeleton
Content you cannot draw in advance, such as a chart or a computed result Spinner scoped to that region
Options inside a select or filter The component’s loading prop
Work with measurable progress, such as uploads or conversions Determinate spinner with an estimate and a way out

Timing

  • Don’t try to predict the duration — it depends on network, payload, and account size.
  • Delay rendering the indicator by 200–500ms, and cancel the timer when the data arrives, so fast responses show nothing at all.
  • Hold the indicator for a few hundred milliseconds once shown, because one that appears and vanishes reads as a glitch.
  • Escalate when a wait keeps going: after roughly ten seconds, add an estimate and a way out.
  • The only duration judgement you need is ordinary wait versus genuinely long operation, and that comes from the nature of the task — loading a list is ordinary, converting a video is long.

Pick a response speed above.

Skeleton

  • Match the real layout when possible: same block sizes, same positions, same number of rows.
  • Only use placeholders for content that changes.
  • Use a skeleton for a first load, not for a refresh of content already on screen.
  • Never leave a skeleton up on failure, because it implies progress that is not happening.
Do

Placeholders mirror the avatar, title, and meta line that will replace them.

Don't

Uniform bars matching nothing — content lands in a different shape and pushes the page around.

Spinner

  • Scope the spinner to the region that is waiting, because a page-level spinner says everything is broken.
  • Show one at a time: several spinners on one screen make the system appear slower.
  • Pass a progress value whenever you can measure it, since an indeterminate spinner only says “still working”.
  • Label it for what it is doing — aria-label is required, and “Uploading” carries more than “Loading”.
  • Don’t block the page unless the user genuinely cannot proceed.

Options in a select or filter

  • Pass loading to SingleSelect or Filter while async options are in flight — it replaces the dropdown body with a spinner.
  • Disable the actions that depend on the pending data, not just the trigger, so no one applies a filter built from options that haven’t arrived.
  • Filter already disables its apply and clear actions while loading is true.

Long operations

  • Give determinate progress, an honest estimate, and a way out — all three together.
  • Use progress between 0 and 100 where you can measure it, and steps such as “Step 2 of 4” where a percentage would be invented.
  • Avoid progress that races to 95% and then hangs, because that is worse than a steady bar.
  • Offer a cancel action, or let the work continue in the background and notify the user when it finishes.
  • The percentage label only renders at sizes 48 and 64, so put the number in adjacent text at smaller sizes.

Convert a 4K video to web-ready formats.

Failure and empty states

  • Build all three exits: loaded with content, loaded with nothing, and failed.
  • Replace the indicator with an error and a retry when a request fails.
  • Use Empty when the request succeeded and the result is genuinely empty.
  • Decide what happens when a request stalls: after a long silence, say so and offer a retry rather than spinning indefinitely.
  • See accessible notifications for choosing between a banner, a dialog, and a page.

Accessibility

  • Announce completion, not just the start — a spinner that unmounts reads as “done” to a sighted user and as nothing at all to a screen reader user.
  • Put one status message in a live region per load, not one per skeleton line.
  • Create the live region before the message is injected, because most screen readers ignore an announcement that arrives together with its container.
  • Set aria-busy="true" on the container being updated so assistive technology can defer announcing a half-built subtree.
  • Add aria-hidden to the placeholder shapes, since they carry no information and the status message does.
  • Use role="status", never role="alert" — LoadingSpinner applies role="status" when indeterminate and role="progressbar" once you pass progress.
  • Move focus deliberately when loading replaces the region the user was focused in, or focus falls to the body and their position is lost.
  • Skeleton renders decorative shapes with no semantics of its own, so the surrounding region has to carry the meaning.

Quarterly report — 42 pages

Announced to screen readers

Nothing announced yet.