Every web application has moments when it is waiting for something: an API response, a database query, a file to finish uploading. What your interface does during those moments shapes how users experience your product more than nearly any other design decision.
A blank white screen for two seconds feels much longer than a skeleton screen for three. A button that goes silent after a click feels broken, while a button that shows a spinner feels responsive. The actual time is the same in both cases. The perception is not. This guide covers how to design loading states correctly, from initial page loads to button feedback to full skeleton screen patterns.

Photo by Vitaly Gariev on Pexels
Why Loading States Deserve a Design System
Most teams treat loading states as an implementation detail rather than a design decision. Developers add a spinner when it seems needed, remove it when the data arrives, and move on. The result is an inconsistent experience where some interactions feel responsive and others feel frozen, depending on who built them.
Loading states are part of the interface. They tell users what is happening, set expectations about how long to wait, and confirm that an action was received. When they are absent or inconsistent, users fill the gap with the worst assumption: that something is wrong.
Building a small loading state system, even just a few patterns defined in one place, removes those inconsistencies. Designers and developers share the same components instead of each inventing their own. The decision of which pattern to use where becomes intentional rather than accidental.
The Four Patterns and When to Use Each
Loading states fall into four categories, and each one answers a different question from the user.
Spinners say: "I received your request, and I'm working on it, but I don't know how long it will take." They are appropriate for short, indeterminate waits where you cannot show progress. The risk with spinners is that they provide no information about whether the wait is one second or ten. If a spinner runs for more than a few seconds, users start to wonder if the app has stalled.
Progress bars say: "I know how much work there is, and I can show you how far along we are." Use them for operations where you genuinely know the percentage: file uploads, multi-step form submissions, batch processing tasks. A progress bar that jumps from 0% to 90% and stalls is worse than no progress bar at all, because it sets a false expectation and then breaks it.
Skeleton screens say: "Content is coming, and it will look roughly like this." They show the layout and structure of the expected content using placeholder shapes before the real data arrives. Skeleton screens reduce perceived load time because users process the structure before the content fills in, which feels faster than watching a blank space. Use them for the initial load of lists, cards, feeds, and data tables.
Inline loaders say: "This specific component is refreshing, but the rest of the page is fine." A row of a data table that is updating, a sidebar widget that is reloading its data, a chart that is refetching after a filter change: these all warrant inline loaders scoped to the component rather than a full-page indicator.
A useful decision rule: if the delay is under 300 milliseconds, show nothing at all. Flashing a spinner in and out for a fast response draws more attention to latency than the latency itself. Set a threshold in your state management so loading indicators only appear when a request has been pending for longer than that window.

Photo by Walls.io on Pexels
Designing Skeleton Screens
The principle behind a skeleton screen is straightforward: show the shape of the content before the content arrives. In practice, the quality of a skeleton screen depends on how closely it mirrors what will actually appear.
Good skeleton screens match the actual layout. If your article card has a header image, a two-line title, a one-line byline, and a three-line excerpt, your skeleton should have a rectangle for the image, two wide bars for the title, a narrow bar for the byline, and three slightly narrower bars for the excerpt. It should not use the same-height bars for everything, because that communicates nothing about what is coming.
For colors, use a neutral gray for text placeholders (something in the range of 10-20% lightness relative to the background). Image placeholders can be slightly darker to communicate that something visual is expected there. Keep contrast low enough that the skeleton reads as placeholder, not as content.
For animation, you have two options: shimmer and pulse. Shimmer moves a gradient highlight horizontally across the placeholders, suggesting light reflecting off a surface. Pulse fades the placeholders in and out. Shimmer is more commonly used and feels slightly more active, which helps on longer loads. Pulse is simpler to implement and less visually aggressive if the skeleton will be visible for only a second or two.
The CSS for a shimmer effect uses a linear gradient background with a background-size wider than the element and a keyframe animation that shifts the background-position:
@keyframes shimmer {
from { background-position: -400px 0; }
to { background-position: 400px 0; }
}
.skeleton-bar {
background: linear-gradient(90deg, #e8e8e8 25%, #f5f5f5 50%, #e8e8e8 75%);
background-size: 800px 100%;
animation: shimmer 1.4s infinite linear;
border-radius: 4px;
height: 1em;
}
Wrap skeleton components in the same structural container as the real content so the layout does not shift when data arrives. If the skeleton is 50px shorter than the loaded content, you will have a jarring jump. Using CSS grid or flexbox in the parent and matching the approximate height of each element prevents this.

Photo by Beate Vogl on Pexels
Button and Form Loading States
Form submission is one of the highest-risk moments in any interface. The user has filled something out, trusted your app with data, and clicked submit. What happens next in the first few seconds determines whether they feel confident or anxious.
The minimum requirement is to disable the button after the first click and show a spinner inside it. Disabling the button prevents double-submit errors, which can cause duplicate records in databases, duplicate charges, or other hard-to-reverse side effects. The spinner confirms that the action was registered.
Maintaining the button's dimensions after the state change matters more than most developers expect. A button that collapses to just a spinner icon causes layout shift, which is visually disruptive and bad for performance metrics. Set an explicit minimum width on the button so swapping the label for a spinner keeps the same footprint.
Add a timeout for hung requests. If a submission does not complete within a reasonable window, maybe ten to fifteen seconds depending on the operation, show an error state and re-enable the button. Leaving the user with a permanently spinning button and no path forward is one of the most frustrating failure modes in web interfaces.
For accessibility, update the button's aria-label to reflect the loading state. Screen reader users need to hear "Submitting" rather than just "Submit" with an unexplained delay.
"Loading states are where we most clearly see whether a product was designed for humans or for happy-path demos. Handling the transitions thoughtfully, including the timeouts and error states that most demos never show, is what separates a product that feels professional from one that feels fragile." - Dennis Traina, founder of 137Foundry
Accessibility Requirements
Loading states create specific accessibility problems when implemented without care. Users navigating with a screen reader receive no visual signal, so they need programmatic announcements instead.
The aria-busy attribute on a container signals to screen readers that the content is loading. Set aria-busy="true" when loading begins and remove it when content arrives. Pair this with an aria-live region elsewhere in the page, typically a visually-hidden div, that announces when loading completes.
Shimmer animations pose a problem for users with vestibular disorders or motion sensitivity. Always wrap skeleton animations in a prefers-reduced-motion media query that disables the animation for users who have requested it:
@media (prefers-reduced-motion: reduce) {
.skeleton-bar {
animation: none;
}
}
The W3C Web Accessibility Initiative at w3.org/WAI publishes detailed guidance on dynamic content and AJAX interactions. Their ARIA authoring practices guide covers live regions and loading indicators specifically.
Color contrast still applies to skeleton screens. If your placeholder is too close in lightness to the background, low-vision users may not see it at all, which leaves them with what looks like a blank screen. A contrast ratio of at least 1.5:1 between the skeleton and the background is a reasonable minimum.

Photo by Peter Olexa on Pexels
Testing Your Loading States
The most common testing mistake is evaluating loading states on a fast internet connection. Your development machine fetches most API responses in under 100 milliseconds, which means skeleton screens flash by too quickly to evaluate. Testing in Chrome DevTools with Network Throttling set to Slow 3G gives you a realistic view of what users on mobile or weak connections experience. The Chrome DevTools documentation includes network simulation in the Network panel.
Test error states as explicitly as you test loading states. Every loading state should have a corresponding error state: what happens when the API returns a 500, when the request times out, when the user's session expires mid-load? Designs that only account for the happy path create dead ends for users when things go wrong.
Run your pages through Google's Lighthouse to check metrics like First Contentful Paint and Largest Contentful Paint. Skeleton screens and lazy-loaded components can improve or harm these numbers depending on how they are implemented. A skeleton that blocks the render of actual content is worse than no skeleton.
Test on actual slow devices if you can. Throttling a fast computer approximates a slow network but not a slow CPU. Budget Android phones take four to five times longer to execute JavaScript than a developer laptop, which means your skeleton-to-content transition may stutter on real devices even if it looks smooth in DevTools.
Common Mistakes to Avoid
Using a full-page spinner for every state is the most widespread pattern. It works, technically, but it prevents content already on the page from being useful while one section loads. Scope your loading indicators to the component that is actually waiting, and leave everything else interactive.
Skeleton screens that look nothing like the content they replace are worse than nothing because they set a false expectation. If you cannot easily match the skeleton to the content structure because the content is too dynamic, a simple pulse animation on a card-shaped container is better than elaborate bars that bear no resemblance to what arrives.
Forgetting the transition is common on teams where front-end and back-end work is siloed. The loading state appears but nothing handles the state change to loaded content, resulting in the skeleton staying visible after data has arrived. Build the loading state and the data-ready state as a matched pair.
Building This Into Your Workflow
Loading states are most reliable when they are part of the design system rather than one-off implementations. Define your skeleton bars, your button loading state component, your inline loader, and your full-page spinner as reusable components. Document when each one is appropriate.
If your team does web development work at any scale, the investment in a small loading state library pays back quickly in consistency, fewer one-off implementations, and a product that feels considered rather than cobbled together.
The MDN Web Docs and Nielsen Norman Group are both good references for the UX research behind perceived performance and how animation and feedback affect user trust. The underlying principle is consistent: users tolerate waiting much better when they understand what is happening and believe progress is being made.
A loading state that communicates clearly is a small thing. But small things that happen on every interaction add up to the overall feeling users have about whether your product respects their time. That feeling is hard to build and easy to lose. The 137Foundry team designs and builds these details as part of every project rather than as an afterthought. See what that looks like in practice on the services page.