Key insights
- Loading is a system: match the pattern to what you actually know about the wait — its shape, its duration, its progress. One default (usually a skeleton) applied everywhere is the tell of a lazy UI.
- Skeletons are for when you know the content's shape — cards, lists, articles — and the wait exceeds ~300ms. They set the right expectation by previewing the layout that's about to load.
- Spinners fit short waits of unknown duration, under ~3s (a button saving, a small fetch). Never stretch one across a full-page load — an endless spinner with no context reads as frozen.
- Progress bars belong to waits over ~3s where you know the percentage — uploads, installs, exports. Pair the bar with real meta (time remaining, speed) so the number earns the user's trust.
- Optimistic UI is the pro move for reversible actions like likes, saves, and bookmarks: update the interface instantly, then reconcile with the server in the background and only surface an error if the sync fails.
- Under ~300ms, show nothing at all. A brief flash of a loading state feels more broken than a slight delay — the eye registers the flicker as a glitch, not as feedback.
Do / Don't
- Do: Pick the pattern from what you know: known shape → skeleton, known percentage → progress, short unknown wait → spinner.
- Do: Apply the response instantly for reversible actions, then sync in the background and roll back only on failure.
- Do: Let sub-300ms responses land with no loading indicator at all.
- Don't: Reach for a skeleton on every fetch regardless of the content shape or how long it takes.
- Don't: Cover a whole page with a spinner for long or open-ended loads.
- Don't: Flash any loading state for a response that resolves in under 300ms.