All posts
Product design

Loading States, Skeletons and Optimistic UI: Making Apps Feel Fast

Loading state design for React apps: when to use skeletons, spinners, progress bars or optimistic UI, how each is built, and when each one misleads users.

By Mehdi Hassan7 min read

The short answer

In loading state design, use a skeleton screen when you know the layout of the content that's coming, a spinner or progress bar for actions whose result has no fixed shape, and optimistic UI when an action almost always succeeds and is easy to undo. Show nothing at all for very fast responses.

Key takeaways

  • Skeletons are for content with a known layout; spinners are for actions; progress bars are for long, measurable work.
  • Delay every indicator slightly, so fast responses never flash a loader.
  • Optimistic UI is only honest when the action rarely fails and the rollback is visible.
  • Keep old content on screen while refreshing, instead of blanking it with a skeleton.
  • Each pattern can lie: a skeleton that never resolves, a fake progress bar or a silent rollback.

What is loading state design?

Loading state design is deciding what users see between asking for something and getting it. It covers page loads, data fetches, saves, uploads and AI responses. Done well, it makes an app feel fast without being faster, which is what people mean by perceived performance.

We're a team that designs and builds in React and Next.js, so this guide pairs each pattern with how it's implemented and, just as important, when it misleads the user. Loading belongs with empty and error states in one family, which we cover in our guide to web app UI design.

When should you use skeleton screens, spinners or optimistic UI?

Choose by what you know about the result. If you know its shape, use a skeleton. If you only know something is happening, use a spinner. If you can measure progress, use a progress bar. If you're confident it will succeed, update optimistically.

Pattern

Use it when

Typical example

How it can mislead

Nothing

The response is near-instant

Toggling a setting, opening a cached page

It can't, as long as the response really is fast

Skeleton screen

The layout of incoming content is known

Dashboard cards, list rows, a profile page

Skeleton never resolves, or the real layout differs and jumps

Spinner

An action is running and the result has no fixed shape

Submitting a form, running a search

Spins forever with no timeout or error

Progress bar

Work is long and measurable

File uploads, imports, exports

Fake progress that crawls and then jumps

Optimistic UI

The action almost always succeeds and is reversible

Liking, starring, renaming, moving a card

Shows success, then silently reverts

Streaming

Output arrives in pieces

AI responses, long reports

Partial output read as final

How do skeleton screens work, and when do they lie?

A skeleton screen renders gray placeholder blocks in the shape of the content that's coming, so the page appears structured immediately and fills in place. It's the right choice for first loads of content-heavy screens, where a centered spinner would leave the user staring at a blank page.

In React, skeletons usually live in Suspense boundaries: each section of the page has its own fallback, so the header and navigation render at once and slower sections fill in independently. In the Next.js App Router, a loading file per route segment does the same at the route level, and streaming lets server-rendered sections arrive as they're ready.

  • Match the real layout. If the skeleton shows three cards and the content has five rows, the page jumps when it loads.
  • Use them for first loads, not refreshes. Replacing visible data with a skeleton on every filter change feels slower than keeping the old data with a small indicator.
  • Don't skeleton everything. One page-wide skeleton is worse than a few section-level ones that resolve independently.
  • Always have an exit. A skeleton must turn into content, an empty state or an error. One that pulses forever is the worst kind of lie.

When is a spinner the right loading indicator?

A spinner is right when the user triggered an action, the result has no predictable shape, and the wait is short. Put it where the action happened, usually inside the button that was pressed, and disable that button so it can't be double-submitted.

Avoid full-page spinners inside an app. They hide everything the user could still read or do, and they make every wait feel like a page reload. If a spinner runs past a few seconds, add text that says what's happening ("Generating your report"), and after a timeout, switch to an error with a retry, using the patterns in our guide to error state UX.

How does optimistic UI work, and when should you avoid it?

Optimistic UI shows the result of an action immediately, before the server confirms it, then quietly reconciles. The user drags a card to Done and it moves at once. If the server agrees, nothing else happens. If it fails, the card moves back and the user is told why.

React 19 includes a hook for this, useOptimistic, which shows a temporary state while an async action runs and falls back to the real state when it settles. Data libraries like TanStack Query offer the same pattern with explicit rollback. The code is the easy part. The design question is what the failure looks like.

Pros

  • Actions feel instant, which matters for frequent small interactions
  • Users keep working without waiting on the network
  • Fits naturally with undo, which you should offer anyway

Cons

  • Silent rollbacks make users doubt what they saw
  • Wrong for payments, sends, deletes and anything irreversible
  • Conflicts with other users' edits need careful handling

Our rule: be optimistic about likes, renames, reordering and toggles. Be honest about money, messages, deletions and anything that affects other people. For those, show a pending state ("Sending") and confirm only when it's real.

What an honest optimistic rollback looks like

For example, suppose a user renames a project in a sidebar. The new name appears instantly. The request fails because another teammate deleted the project a moment earlier.

A dishonest rollback quietly restores the old name, and the user wonders whether they imagined the edit.

An honest rollback restores the old state and shows a message anchored to the item: "Couldn't rename: this project was deleted by Sam." with a link to Trash. If the failure is temporary, offer a retry that reapplies the change.

The principle: optimistic UI is a promise. When you can't keep it, say so where the user was looking.

How should loading states handle refreshes and AI responses?

On refreshes, keep showing the old content and add a small, non-blocking indicator, such as a thin bar at the top of the table or dimmed rows. Blanking a table with a skeleton every time a filter changes makes the app feel slower and loses the user's place. Our guide to filter UX design covers this for search and filter changes specifically.

AI responses need their own loading language. Stream text as it arrives, show which step an agent is on ("Searching your docs", "Drafting"), and make the stop button obvious. Distinguish "thinking" from "writing" so users know whether to wait or read. Our guide to AI product UX goes further on progress and uncertainty.

What are the most common loading state mistakes?

The most common loading state mistakes are showing the wrong state, not ending the state, and moving the layout. Each one makes a fast app feel unreliable.

  • Flashing an empty state while data loads. "No projects yet" for a moment, then a full list, tells users their data briefly disappeared. Render loading until you know it's empty, then use the patterns from our guide to empty state design.
  • Layout shift. Content that pushes everything down as it arrives. Reserve the space with skeletons or fixed dimensions.
  • No timeout. Every indicator needs a maximum duration after which it becomes an error.
  • Silent loaders for screen readers. Mark loading regions as busy and announce when content is ready, as covered in our accessible web app design checklist.
  • Animation that's louder than content. Respect reduced-motion settings and keep shimmer subtle.

Frequently asked questions

Are skeleton screens better than spinners?
Neither is better in general; they solve different problems. Skeleton screens suit first loads of content whose layout you know, because the page looks structured right away. Spinners suit actions with an unpredictable result, like submitting a form. Using a skeleton for an action, or a full-page spinner for content, is where most loading states go wrong.
What is perceived performance?
Perceived performance is how fast an app feels to the person using it, as opposed to how fast it measurably is. Showing structure early, responding to clicks immediately, keeping old content visible during refreshes and avoiding layout jumps all improve it. It complements real speed work rather than replacing it, because a slow backend still eventually shows through.
Should loading indicators be animated?
Yes, a subtle animation signals that the app is working rather than frozen, but keep it calm. A gentle shimmer on skeletons or a small spinner is enough. Respect the user's reduced-motion setting by switching to a static or very slow indicator, and avoid large looping animations that pull attention away from the content that's arriving.
How do you test loading states?
Throttle the network in your browser's developer tools and click through the app on a slow connection, because loading states are invisible on a fast office network. Also force failures and long delays on purpose to check that every indicator ends in content, an empty state or an error. Storybook stories for each state help designers review them.
Is optimistic UI safe for forms?
Optimistic UI is safe for small, reversible edits inside a form, like toggling a setting or renaming an item, where failure is rare and easy to show. It isn't appropriate for submitting payments, sending messages or creating accounts. For those, show a clear pending state on the button and only confirm success after the server has responded.

· CEO & Design Director

Copywriter, designer and developer, and founder of Designify Studio. Over five years, 60+ projects across 14+ industries, for startups that have raised $300M+.

Want this done for you? Brand & design sprint, $3,000.

A brand refresh or redesign, plus design explorations for your website and product. $3,000, one-time, fixed price. Book a free 30-minute call to see if it fits.

Schedule Call