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.
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.
40%
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.