UIGuides

How to Design Loading States

5 min read

Loading state design guide — skeleton screens vs spinners vs progress bars, perceived performance techniques, optimistic UI, error states after timeout, and prototyping in ProtoPie.

Loading states are one of the most underdesigned parts of most products. Designers nail the success states and the empty states, then wave at loading as "just show a spinner." But users spend real time in loading states, and that experience shapes how fast your product feels — regardless of the actual network speed.

Here's how to design loading states that make your product feel faster.

Skeleton screens vs spinners vs progress bars

These three patterns solve different problems. Using the wrong one makes things feel slower than they are.

Skeleton screens are placeholder layouts that mirror the shape of the content being loaded. They look like the page you're about to see, but with gray blocks where text and images will be. Use them when: you're loading a page or section with predictable layout (a feed, a dashboard, a profile page). The reason they feel faster: users' brains start parsing the layout while the real content loads. The interface seems to be "filling in" rather than appearing from nothing.

Spinners (a rotating indicator) are appropriate for short loading states (under 1 second) and for actions within a page (submitting a form, running a search, triggering an action). Use them when: the wait is brief, the layout isn't predictable, or the loading context is too small for a skeleton. Don't use a full-screen spinner for a page load — it leaves users with a blank context.

Progress bars are for operations with measurable duration: file uploads, bulk operations, multi-step processes where you can actually calculate percentage complete. Use them when: you have real progress data to show. A fake progress bar (one that doesn't reflect actual progress) is worse than no progress bar — users notice when it slows down or freezes.

Perceived performance: making things feel faster

Actual load time and perceived load time aren't the same number. Perceived performance is designable.

Start rendering immediately. Show the page shell (navigation, header, basic layout) before the content loads. Users see something immediately instead of a blank screen.

Load content above the fold first. If you're fetching a long page, prioritize the data for the visible viewport. Users can start reading while the rest loads.

Use progressive loading. Load low-resolution or placeholder content first, then replace it with full-quality content as it arrives. This is common for images (blur-up technique) and text-heavy content.

Match loading patterns to content. A skeleton that has 3 text lines tells the user "expect 3 items." If the real content has 8 items, the jump feels jarring. Generic skeletons (a few blocks of gray) are safer than overly specific layouts you can't guarantee.

Skeleton screen patterns

A good skeleton design in Figma:

  • Use slightly rounded rectangles to represent text lines (a rectangle of roughly 12-16px height)
  • Use a square or rounded rectangle for image/media blocks
  • Apply a subtle shimmer animation (the skeleton items appear to have light passing across them from left to right — this communicates activity to the user, not a frozen interface)
  • Keep skeleton colors subtle: gray-100 or gray-200 as the base, gray-200 or gray-300 as the shimmer highlight

The shimmer animation needs to be prototyped to communicate the intent to developers. ProtoPie handles this elegantly — you can build a shimmer effect with a moving gradient mask and loop it.

Try ProtoPie

Optimistic UI

Optimistic UI means showing the result of an action before the server confirms it. When a user adds a reaction to a post, the count increments immediately in the UI. The actual API call happens in the background. If it fails, you roll back with an error state.

Design for optimistic UI:

  • Design the success state (the UI with the action applied)
  • Design the rollback state (the UI after a failure — return to previous state + inline error message)
  • Note in your annotations which actions are optimistic vs which require server confirmation before updating the UI

Optimistic UI makes products feel significantly faster for common, low-risk actions (likes, follows, adding items to a cart). Don't use it for high-stakes actions (payment submission, account deletion) where users need confirmation the action completed.

Error state after timeout

Every loading state needs a timeout error state. What happens after 10-30 seconds if the content still hasn't loaded?

Design and spec:

  • A clear error message (not "Error 503" — something like "We couldn't load this content. Check your connection and try again.")
  • A retry action (a button, not just text)
  • The correct visual context (not a blank page with an error toast — the error should be contextual to what failed)

Annotate the timeout duration in your spec. Developers need to know when to trigger the error state. Industry standards: show an inline loading indicator up to 1 second, switch to skeleton screens for 1-10 seconds, show error state at 10-30 seconds depending on operation type.

Micro-copy for loading messages

Static "Loading..." is fine. Contextual messages are better.

Examples:

  • "Finding available slots..." (calendar booking)
  • "Preparing your export..." (file download)
  • "Almost done..." (near end of a longer process)

Short, specific, present-tense. Don't make promises about time ("only a few more seconds") — they'll be wrong at least some of the time and frustrate users when they are.

Design in Figma

Build a loading states component library in Figma that covers: spinner (small/medium/large), skeleton blocks (text line/image block/card placeholder), progress bar, and button loading state (for form submit actions). This makes it easy to drop the correct loading treatment into any new design without reinventing it each time.