Loading States in UI: Spinners, Skeletons and Progress Bars
Practice4 minInnovialabs editorial team
In short
Choose a loading indicator by expected wait: under a second show nothing, a few seconds use a skeleton or spinner, longer waits need a progress bar with a clear status. Always design error, timeout and partial-loading states too.
Which indicator to use
The main criterion is how long the user waits and whether the amount of work is known. Common rules of thumb:
| Wait | What to show |
|---|---|
| Under ~1 second | Nothing, or an instant button response. An indicator would only flash and make things feel slow |
| 1–3 seconds, screen layout is known | Skeleton — grey blocks where content will appear |
| 1–10 seconds, layout unknown or action is local | Spinner on the button or in the area being updated |
| Over ~10 seconds, or the amount of work is known | Progress bar with percentage or steps, plus a way to cancel |
Skeletons suit list and card screens, spinners suit actions like "Save", progress bars suit uploads, exports, imports and generation.
Perceived performance
Perceived performance is how fast the product feels, not how many milliseconds it actually takes. You can improve it without touching the server:
- Instant feedback. The button changes state right after the tap, even while the request is in flight.
- Stable layout. A skeleton mirrors real block sizes, so content does not jump when it arrives.
- Delay before the indicator. If a response may come quickly, show the spinner after a short pause (typically 200–500 ms). Once shown, keep it for a minimum time to avoid flicker.
- Most important first. The heading and key data arrive first; secondary content follows.
Optimistic UI
Optimistic UI shows the result of an action before the server confirms it. You tap like and the heart fills instantly; you add a task and it is already in the list.
It fits when:
- the action almost always succeeds (likes, mark as read, reordering cards);
- it is easy to undo.
It does not fit payments, submitting applications or deletions without a trash bin — there the user must see the real status.
The mandatory part of the pattern is rollback: if the server returns an error, the element goes back to its previous state and the user sees a clear message rather than a change that silently vanished.
Partial loading
A screen rarely loads in one request. Each block should be able to stand on its own:
- widgets load independently, so one slow block does not hold up the whole screen;
- long lists load in batches with an indicator at the bottom or a "Show more" button;
- images reserve space in advance and show a placeholder or blurred preview;
- when refreshing data that is already loaded, keep the old content visible and show the indicator on top or next to it — do not switch back to a skeleton.
Errors and timeouts
A loading state without an error path is unfinished design. For every block, decide:
- Network or server error: what happened in plain words and a "Retry" button.
- Timeout: after how many seconds to stop waiting and what to tell the user. An endless spinner is the worst option.
- Long wait: if the operation drags on, update the text ("Almost there, processing your file") so it is clear the process is alive.
- Empty state: loading succeeded but there is no data. That is a separate screen, not an error.
- Offline: what is available without a connection and what happens to actions taken meanwhile.
How to hand it off to developers
So that states are not invented on the fly, document them in mockups and the spec:
- State matrix for every screen and component: idle, loading, success, empty, error, partial.
- Indicator type for each action and where it appears.
- Timings: delay before showing, minimum display time, timeout.
- Skeleton components in the design system, sized like the real cards.
- Copy for errors, timeouts and long waits.
- Error behavior for optimistic UI: what the rollback looks like.
- Animations: duration and how they are disabled under
prefers-reduced-motion.
Common mistakes
- A full-screen spinner while refreshing a single block.
- A skeleton that does not match the real layout.
- An indicator with no timeout and no error message.
- A progress bar that freezes at 99% — an honest step status is better.
- Blocking the whole interface when the user could keep working.