Loading states are one of the easiest parts of a product to ship carelessly, and one of the first things users notice when they are done well. Most teams treat the loading state as the gap between request and response — a blank page, a spinner, maybe a logo. The better teams treat it as an opportunity to say something about the product before the product has a chance to say anything itself.
This post is a field guide. I want to lay out the patterns I see in the wild, what each one is good for, what each one gets wrong, and how to pick between them. It is not exhaustive, but it covers the shapes you will see over and over again once you start looking.
The Spinner, and Why It Usually Loses
The default loading state, for twenty years running, has been the spinner. It is in every framework, every UI kit, every starter template. It is familiar. It is universal. And in almost every case I can think of, it is the wrong choice.
A spinner communicates exactly two things: "something is happening" and "I cannot tell you what." That is useful when the something is very brief, or when the context makes the outcome obvious. It is not useful as a primary loading pattern, because it provides no information about what is about to appear, how long it will take, or whether anything is even happening at all beyond the animation.
The temptation to use a spinner is that it is the cheapest possible option. The cost is that it trains users to treat your product as a waiting room. Reserve spinners for inline operations that are too short to deserve a skeleton — form submissions, toggles, "working on it" feedback within a button — and find something better for full-page loads.
Skeletons
Skeletons are the pattern I reach for most often, and the one I recommend as a default for any list, card grid, or structured content view.
A skeleton is a placeholder that matches the silhouette of the content that is about to appear. Rectangles where text will be, circles where avatars will be, empty boxes where images will be. The user sees the shape of the page before the page has loaded, which does three useful things: it reserves layout space (preventing shifts when content arrives), it signals what is coming, and it turns passive waiting into anticipatory waiting.
The most common mistake with skeletons is making them too faithful to the final content — animating shimmer across every pixel, rendering near-real placeholder text, varying widths to imitate paragraph rhythm. None of this is necessary. A skeleton is a silhouette, not a preview. Keep it simple, keep it uniform, and let the real content be the reward.
The second most common mistake is using skeletons for operations that are either too short to need them, or too long to survive them. A skeleton that flashes for 80ms feels like a glitch. A skeleton that sits unchanged for 30 seconds feels like the page is broken. The sweet spot is roughly 300ms to 3 seconds — long enough to register, short enough to pay off.
Optimistic and Immediate
The fastest-feeling loading state is the one that does not happen. Optimistic UI updates the interface the moment the user initiates an action, as though the action had already succeeded — and the actual request happens in the background.
This pattern is overwhelmingly appropriate for low-stakes, high-success-rate interactions: likes, favourites, reactions, "mark as read," adding items to a list, quick edits to local state. The perceptual benefit is enormous — zero perceived latency — and the cost, a small reconciliation routine for the rare failure case, is almost always worth paying.
The thing to avoid is optimism in domains where silent failure is unacceptable. Payments, publishes, deletions, anything with external or permanent consequences — these deserve honest acknowledgment of the request, not a comforting fiction. The test is simple: if this operation fails and the user does not notice, is anyone harmed? If yes, don't be optimistic.
Progressive Disclosure
For content-heavy views — dashboards, profiles, feeds, anything where different parts of the page have different latency — the best pattern is often progressive disclosure. Render what you have as soon as you have it, and fill in the rest as it arrives.
A profile page might load the user's name and avatar in 100ms, their recent activity in 400ms, and their full post history in 2 seconds. Instead of waiting for everything before showing anything, render each section as it resolves. The user gets immediate confirmation that the page is loading, something to look at while they wait for the rest, and a sense of momentum that a single long blocking load will never produce.
Progressive disclosure gets harder to implement as the number of data sources grows, which is why it is relatively rare in the wild despite being superior in most cases. React Server Components, Suspense boundaries, and streaming responses have made it dramatically cheaper than it used to be. If your stack supports it, use it — it is the pattern that scales best as products get content-heavier.
When loading is genuinely long — more than about 5 seconds — all of the above patterns start to falter. The user starts to doubt whether anything is happening. A skeleton that has not resolved in 10 seconds looks indistinguishable from a broken page. A spinner at 30 seconds looks indistinguishable from a crash.
The fix is to narrate the work. Break the operation into named stages and surface them to the user, one at a time: "uploading," "processing," "finalizing." The total time does not change, but the user's experience shifts from "am I stuck?" to "where am I in the process?" — a question that is much more comfortable to sit with.
The honesty principle applies doubly here. A progress bar that fills to 95% and stalls is worse than no progress bar. A "processing…" message that never changes is worse than a spinner. If you cannot predict time accurately, prefer step-based progress over percentage-based. Stages move forward at a rate the user can verify; percentages demand trust you may not be able to earn back.
Making the Loading State Yours
Here is the thing the patterns above don't capture on their own: the loading state is one of the few moments in a product where you have the user's full attention and no competition for it. There is nothing else on screen. They are waiting, and they are looking.
The products I admire most use this window deliberately. Linear loads its sidebar with a signature animation that is unmistakably theirs. Notion's skeletons have a rhythm that matches the rest of the product's calm. The best loading states feel like the product introducing itself before the content arrives.
This does not mean cluttering the loading state with brand flourishes. It means treating the loading state as a design surface — not a blank gap to tolerate, but a small stage to use with intent. A custom skeleton can do it. A deliberate empty state can do it. A single well-chosen motion can do it. What all of these have in common is that the team decided the loading state mattered, and designed it on purpose.
Footnotes
The Nielsen Norman Group's article on response times established the 100ms / 1s / 10s thresholds that still frame every conversation about loading.
Luke Wroblewski's Mobile First touches on skeleton-style patterns before they had that name, and his reasoning still holds up.
The Facebook engineering blog has multiple good posts on why they moved from spinners to skeletons. The research they did on perceived performance is worth tracking down.
Vercel's documentation on streaming with Suspense is the clearest guide I've seen for implementing progressive disclosure in practice.
The Linear loading animation is an example I come back to often when I want to remember what signature loading looks like. Emil Kowalski has recreated it as a learning exercise.