How to Design Loading States That Don't Feel Like Lying to Users

A tablet screen displaying a loading interface with placeholder content blocks

Every loading state makes an implicit promise: something is happening, and it will finish soon. When that promise does not match reality, users notice immediately, even if they cannot articulate why. A spinner that spins for eight seconds after promising a quick wait feels worse than an eight second wait with no spinner at all, because the spinner suggested the whole time that it was almost done.

Loading states are one of the most under-designed parts of a product. Teams spend weeks polishing the primary interface and minutes on the states between pages, tabs, and data fetches. That gap shows up later as support tickets about "the app feeling slow," even when the actual network numbers look fine.

Why Perceived Performance Is Not the Same as Actual Performance

Two products can have identical load times and completely different reputations for speed. The difference is almost always in what the user sees while waiting, not the raw milliseconds.

Nielsen Norman Group has documented three response time thresholds that still hold up: under 0.1 seconds feels instantaneous, up to 1 second keeps the user's flow of thought uninterrupted, and past 10 seconds the user's attention has left the task entirely. Most meaningful loading states fall in the middle zone, between 1 and 10 seconds, where the interface has to actively manage the wait rather than just complete it fast enough to avoid the problem.

The tools available in that middle zone are limited but effective when used correctly:

  • Immediate feedback the instant an action is taken, before any data has actually loaded
  • Progress communication that reflects something real, not a decorative animation
  • Content structure preview so the eventual layout does not shift once real content arrives

Skipping any of these does not make the wait shorter. It just makes the wait feel unmanaged, and unmanaged waits are the ones users complain about.

hand holding a tablet showing a touchscreen interface
Photo by Wolf Art on Pexels

Spinners: Use Them for Genuinely Unknown Durations

A spinner communicates one thing clearly: work is happening, and the interface does not know how long it will take. That is a legitimate message for genuinely unpredictable operations, like a search query against an unindexed dataset or a third-party API call with variable latency.

Spinners fail when they are used for operations that are actually predictable. If your application always takes roughly the same 800 milliseconds to fetch a user's dashboard data, a spinner is the wrong tool. Predictable operations deserve a state that reflects the structure of what is coming, not a generic "working on it" indicator.

Practical spinner rules:

  • Do not show a spinner for operations under 400 milliseconds. The flash of a spinner appearing and disappearing almost instantly reads as visual noise, not helpful feedback.
  • If an operation regularly exceeds 5 seconds, a bare spinner is not enough. Add a text label that updates ("Fetching records," then "Processing results") so the user has evidence the system is still working, not stuck.
  • Never combine a spinner with a fake progress percentage. If you do not know the real progress, do not simulate one. Users learn to distrust progress bars that jump from 10% to 95% in one frame.

Skeleton Screens: Preview the Structure Before the Content

A skeleton screen shows gray placeholder blocks in the shape of the content that is about to load: a rectangle where an image will appear, lines where text will appear, a circle where an avatar will appear. The technique works because it lets the user's brain start parsing the layout before the actual data arrives, which reduces the perceived wait compared to a blank screen followed by a sudden content pop-in.

Skeleton screens work best when:

  • The layout is genuinely predictable (a list of cards, a table, a profile page) so the placeholder shapes closely match the real content
  • The content loads within roughly 1 to 3 seconds; beyond that, a static skeleton starts to feel like it stalled
  • The placeholder blocks use a subtle shimmer or pulse animation, not a static gray box, so the user can distinguish "loading" from "broken"

Skeleton screens are the wrong choice when the eventual layout is unpredictable, like a search results page where the number and type of results genuinely varies. A skeleton that promises three uniform result cards, followed by five results of varying heights, creates the same layout shift problem the technique was supposed to prevent. web.dev has detailed guidance on measuring and reducing this kind of layout shift as part of Core Web Vitals.

close-up of a smartphone screen showing an app interface
Photo by iam hogir on Pexels

Progress Bars: Only for Operations With Real, Measurable Progress

A progress bar implies the system can measure how much work remains. Use one only when that is literally true, such as a file upload where you know bytes sent versus total bytes, or a multi-step import where you know which step is currently running out of a known total.

Do not use a progress bar to represent an operation where progress cannot actually be measured. A database query either finishes or it does not; there is no "62% done" state for it in most systems. Faking that number erodes trust faster than a plain spinner would, because it specifically promises information the interface does not have.

When progress bars are appropriate, a few details matter:

  • Show the actual unit when it helps ("14 of 32 files uploaded") rather than only a bar
  • Never let the bar move backward or stall visibly; if an estimate turns out wrong, smooth the correction rather than exposing the jump
  • Pair the bar with an estimated time remaining only if that estimate is reasonably stable; a wildly fluctuating time estimate is worse than no estimate at all

"The teams that get loading states right are usually the ones who stopped treating them as a design afterthought and started treating them as a data problem. If you know what the system actually knows at each moment, whether that's real progress or genuinely nothing, the right pattern becomes obvious." - Dennis Traina, founder of 137Foundry

Handling Slow and Failed Loads Gracefully

Every loading state design needs an answer for what happens when loading takes longer than expected or fails outright. Products that only design the happy path end up with spinners that spin forever when a request silently fails, which is worse for trust than almost any other failure mode.

A workable pattern:

  1. Show the normal loading state (skeleton, spinner, or progress bar) up to a reasonable timeout, typically 8 to 15 seconds depending on the operation
  2. If the timeout is reached without a response, replace the loading state with a message that acknowledges the delay and offers a retry action
  3. If the request fails outright, show a distinct error state, not a stalled loading state. Users should never have to guess whether something is still working or has quietly died

Accessibility matters here as much as visual design. Screen reader users cannot see a spinner animation, so the loading and completion states need to be announced programmatically. MDN's documentation on ARIA live regions covers how to use aria-live="polite" to announce state changes like "Results loaded" without interrupting whatever the user is currently doing, and WebAIM has practical examples of applying this to real loading and error states.

hand holding a smartphone with a lit screen
Photo by Luis Quintero on Pexels

Matching the Loading Pattern to the Interaction

Not every loading moment deserves the same treatment. A useful way to sort them:

  • Page-level navigation (moving from one route to another): a skeleton screen matching the destination layout, or a thin progress bar at the top of the viewport if the destination structure is not predictable in advance
  • In-place data refresh (pulling to refresh a feed, re-running a filtered search): a small, localized spinner near the affected content, not a full-page takeover
  • Button-triggered actions (submitting a form, saving a record): disable the button and show an inline spinner or label change ("Saving...") directly on the control that was clicked, so the feedback is spatially tied to the action
  • Background operations the user did not directly trigger (auto-save, background sync): a subtle, unobtrusive indicator, often in a status bar or toast, that does not block interaction with the rest of the interface

Mismatching these, like using a full-page blocking spinner for a background auto-save, creates the impression that the whole application has frozen over something the user never asked to wait for.

whiteboard covered in sketched diagrams and arrows
Photo by Christina Morillo on Pexels

At 137Foundry, loading state design comes up in nearly every web development engagement, usually after a client notices that their product feels slower than its actual performance numbers justify. The fix is rarely a backend optimization. It is almost always a mismatch between what the interface promises during a wait and what is actually happening behind it.

Getting this right is part of the broader set of interface decisions we work through with clients across our services, from initial architecture through the small interaction details that end up shaping how fast a product feels in daily use.

A Short Checklist Before You Ship a Loading State

  1. Does the loading pattern match what the system actually knows (real progress, unknown duration, or predictable structure)?
  2. Is there a timeout and a distinct error state, so a failed request never looks identical to a slow one?
  3. Are state changes announced to screen readers through ARIA live regions, not just communicated visually?
  4. Does the loading state's shape match the eventual content closely enough to avoid a jarring layout shift?
  5. Is the loading treatment scoped to match the interaction, rather than defaulting to a full-page spinner for every wait?

Loading states are small pieces of interface, but they run constantly, on nearly every screen, for every user. Getting the pattern right once and applying it consistently does more for a product's perceived quality than most visible feature work.

Need help with Web Development?

137Foundry builds custom software, AI integrations, and automation systems for businesses that need real solutions.

Book a Free Consultation View Services