Skip to content
HomeWorkProcessAboutBlogContact[email protected]
← Back to blog

Loading State Design: Making Wait Times Feel Shorter

Loading State Design: Making Wait Times Feel Shorter. loading state design Insights from The Proper Motion Company.

The difference between an application that feels fast and one that feels slow is rarely about actual speed. It is about perceived speed. A 200-millisecond operation with no visual feedback feels slower than a 500-millisecond operation with a well-designed loading state. The psychology of waiting is as important as the engineering of performance, and the companies that understand this build products that feel responsive even when they are not.

The Psychology of Perceived Performance

Research from the Nielsen Norman Group established that users perceive response times in three tiers:

  • Under 100 ms: Feels instantaneous. The user perceives no delay.
  • 100 ms to 1 second: Noticeable but acceptable. The user feels the system is working.
  • Over 1 second: The user’s flow is broken. Attention shifts. Anxiety increases.

But these thresholds are not fixed. They are influenced by what the user sees during the wait. A study published in the International Journal of Human-Computer Studies found that progress indicators reduced perceived wait time by up to 40% compared to a static spinner, even when the actual wait was identical.

The key insight is that uncertainty amplifies perceived duration. When a user clicks a button and nothing happens for 800 milliseconds, their brain starts asking questions: Did my click register? Is the app frozen? Should I click again? Each question adds cognitive load and makes the wait feel longer. A loading state is not decoration. It is an answer to those questions.

Facebook’s engineering team discovered that replacing their news feed’s loading spinner with content-shaped placeholder skeletons reduced perceived load time significantly, even though the actual time-to-interactive remained the same. Users reported the experience felt faster because the transition from empty to populated was less jarring.

Related: Designing Onboarding Flows That Reduce Churn

Skeleton Screens: The Gold Standard

Skeleton screens are placeholder UI elements that match the shape and layout of the content being loaded. Instead of a spinner in the center of the page, the user sees gray blocks where text will appear, rounded rectangles where images will load, and outlined shapes where buttons will render.

The implementation is straightforward. For a card component that will display a user profile:

.skeleton-text {
  background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  border-radius: 4px;
  height: 16px;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

The shimmer animation is critical. A static gray block looks like a broken UI element. A shimmering block communicates “content is on its way.” The direction of the shimmer (left to right in LTR interfaces) reinforces the sense of progress.

Design skeleton screens to match your actual content dimensions closely. If your card titles are typically 40-60 characters, make the skeleton text block that width. If your avatars are 48x48 pixels, make the skeleton circle 48x48. Mismatched dimensions cause a jarring “jump” when real content replaces the skeleton, which undermines the smoothness you are trying to create.

Libraries that implement skeleton screens well include react-loading-skeleton for React, ngx-skeleton-loader for Angular, and vue-content-loading for Vue. For server-rendered applications, you can embed skeleton HTML directly in the page template so it appears before JavaScript even loads.

Progressive Loading and Content Prioritization

Not all content on a page is equally important. Progressive loading renders high-priority content first and fills in secondary content afterward. This is not lazy loading in the performance optimization sense, though they are related. It is a UX strategy that ensures the user can start working before everything is ready.

Above-the-fold prioritization. Content visible in the initial viewport should load before content below the fold. For a dashboard, this means the primary metric cards load before the charts further down the page. For an email client, the inbox list loads before the folder sidebar.

Text before images. Text renders faster and provides more information density than images. Load text content first, then populate images. The <img loading="lazy"> attribute handles images below the fold natively in modern browsers. For above-the-fold images, use fetchpriority="high" and preload hints.

Interactive elements before decorative elements. A user needs the search bar, navigation, and primary action buttons immediately. Background images, promotional banners, and secondary sidebar content can load after. Prioritize the elements that let users take their next action.

Staggered data fetching. Instead of one massive API call that blocks the entire page, break data fetching into multiple calls ordered by priority. React’s Suspense boundaries make this elegant: wrap each section of your page in its own Suspense boundary with a fallback skeleton, and each section loads independently as its data arrives. This pattern replaced the “blank page until everything loads” approach that plagued single-page applications for years.

An e-commerce product page might fetch data in this order: 1. Product name, price, and primary image (above-the-fold essentials) 2. Product description and specifications (below-the-fold text content) 3. Customer reviews summary (social proof) 4. Related products carousel (cross-sell, lowest priority)

Each section renders as soon as its data arrives, with skeleton placeholders for sections still loading.

See also: How to Run a Design Sprint for Product Development

Optimistic Updates and Instant Feedback

The fastest loading state is no loading state at all. Optimistic updates assume the server will succeed and immediately reflect the user’s action in the UI, rolling back only if the server returns an error.

This pattern is appropriate when: - The success rate exceeds 99% (most CRUD operations on a healthy system). - The action is reversible (sending a like, adding an item to a list, toggling a setting). - The consequence of a temporary incorrect state is low (showing a like count of 43 instead of 42 for 200 ms is acceptable).

This pattern is not appropriate when: - The operation has financial implications (payment processing, fund transfers). - The operation is irreversible (deleting an account, sending a message to an external system). - The success rate is uncertain (operations that depend on external APIs with variable reliability).

For appropriate cases, the implementation follows a consistent pattern. When the user clicks “favorite” on an item:

  1. Immediately update the local state to reflect the new favorite status.
  2. Render the filled heart icon instantly.
  3. Send the API request in the background.
  4. If the request succeeds, do nothing. The UI is already correct.
  5. If the request fails, revert the local state and show a non-intrusive error notification.

React Query, SWR, and Apollo Client all have built-in support for optimistic updates with automatic rollback on failure. The key UX detail is that the rollback should be smooth, not jarring. Animate the state change back rather than snapping to the previous state.

Micro-Interactions That Communicate Progress

Between the extremes of “no feedback” and “full skeleton screen,” there is a rich vocabulary of micro-interactions that communicate progress for small, fast operations.

Button loading states. When a user clicks a submit button, replace the button text with a small spinner or animated dots and disable the button. This prevents double-submission and confirms the click was registered. The button should maintain its exact dimensions to prevent layout shifts. Measure the button width before the state change and set it explicitly.

Inline progress for uploads. File uploads should show a progress bar with percentage and estimated time remaining. The browser’s XMLHttpRequest.upload.onprogress event or the Fetch API’s ReadableStream provides real progress data. Never use a fake progress bar that advances on a timer for uploads; users can tell when progress does not correlate with their network speed.

Transition animations for route changes. When navigating between pages, a brief fade or slide transition of 150-300 ms masks the rendering delay and creates a sense of spatial continuity. Longer transitions feel sluggish. Shorter ones look glitchy. The View Transitions API now supported in Chrome and Edge makes this trivial to implement without JavaScript animation libraries.

Toast notifications for background operations. When an action triggers a background process (exporting a report, syncing data), show an immediate toast notification: “Export started. We will notify you when it is ready.” This acknowledges the action and sets expectations about timing. Follow up with a second notification when the process completes.

Pulsing indicators for live data. When a dashboard metric updates in real time, a brief pulse animation on the changed value draws attention to the update without being distracting. A subtle background color flash that fades over 500 ms works well. Avoid blinking or bouncing animations that become annoying when data updates frequently.

Measuring Perceived Performance

You cannot improve what you do not measure. Traditional performance metrics like Time to First Byte (TTFB) and DOMContentLoaded are server and browser metrics. They do not measure what the user experiences.

The metrics that matter for perceived performance:

First Contentful Paint (FCP). When the first piece of content renders. This is when the blank white screen ends and the user sees something. Target under 1.8 seconds.

Largest Contentful Paint (LCP). When the largest visible content element renders. This is when the page feels “loaded.” Target under 2.5 seconds.

Cumulative Layout Shift (CLS). How much the page layout shifts during loading. High CLS means content jumps around as new elements load, which is disorienting and frustrating. Target under 0.1. Skeleton screens help here: they reserve space for content, preventing shifts when real content replaces them.

Interaction to Next Paint (INP). How quickly the page responds to user interactions. This measures the perceived responsiveness after the page has loaded. Target under 200 ms.

Use Google’s web-vitals JavaScript library to capture these metrics from real users. Send the data to your analytics platform and segment by device type, connection speed, and geographic region. A dashboard that loads in 1.2 seconds on a developer’s MacBook Pro with fiber internet might take 4.5 seconds on a customer’s Chromebook on hotel Wi-Fi.

Run A/B tests on loading state designs. Measure not just performance metrics but business metrics: task completion rate, session duration, and conversion rate. We have seen skeleton screen implementations increase conversion rates by 8-12% compared to spinner-based loading, even with identical actual load times. Perception drives behavior.


Want to make your application feel faster without rewriting the backend? Let us design loading experiences that keep your users engaged.

// Related posts