A blank screen while data loads reads as broken, not in-progress. This lesson covers the real toolkit for loading UI: when a simple spinner is enough, when skeleton screens reduce perceived wait time, honest progress bars, and making loading states accessible to screen readers.
1A Blank Screen Is Never the Right Answer
Showing nothing at all while data loads β a blank area with no indicator β reads to users as broken rather than in-progress. Every loading state is a genuine design decision about what a user should see, and for how long, before real content is ready.
2Spinners: Simple, but Communicate Little
A spinner is the simplest loading indicator, communicating that something is happening but nothing about how much progress has been made or how long it will take. It's appropriate for quick operations, but starts feeling like uncertainty rather than progress for longer waits.
3Skeleton Screens: Showing the Shape of What's Coming
A skeleton screen renders placeholder blocks matching the layout of the actual content β a shape where an avatar will be, lines where text will appear. This reduces perceived wait time by letting users understand the page's structure before real data arrives.
4Progress Indicators for Long, Measurable Operations
When an operation's completion percentage is genuinely known β like bytes transferred during a file upload β a real progress bar is more honest and reassuring than a generic spinner. A fabricated or inaccurate progress percentage erodes user trust faster than an honest spinner would.
5Accessible Loading Announcements
A visual spinner alone provides no information to screen reader users unless the loading region uses aria-live='polite' (or role='status') and aria-busy='true' while content is being replaced, ensuring the loading state is communicated beyond just visual cues.
6Step-by-Step Breakdown
A Blank Screen Is Never the Right Answer. While data is loading, showing nothing at all β a blank white area β reads as broken, not 'in progress.' Every loading state is a design decision: what should the user see, and for how long, before the real content is ready?
Spinners: Simple, but Communicate Little. A spinner is the easiest loading indicator to build and communicates 'something is happening,' but nothing about HOW MUCH is happening or how long it'll take. For quick loads (under ~1 second), that's fine. For anything longer, a spinner alone starts to feel like uncertainty rather than progress.
Why does a spinner alone start to feel inadequate for a loading operation taking several seconds?
- βIt communicates nothing about how much progress has been made or is left
- βSpinners always animate too quickly for slow connections
Skeleton Screens: Showing the Shape of What's Coming. A skeleton screen renders gray placeholder blocks matching the LAYOUT of the real content β a rectangle where the avatar will be, lines where text will appear. This reduces perceived wait time because the user already understands the page's structure before any real data has arrived.
Progress Indicators for Long, Measurable Operations. When you actually know how much of an operation has completed β a file upload reporting bytes transferred β a progress bar with a real percentage is far more honest and reassuring than a generic spinner. Never fake a progress percentage; an inaccurate progress bar erodes trust faster than an honest spinner.
When is a real, numeric progress bar the appropriate loading indicator instead of a generic spinner?
- βWhen you actually know a real, measurable percentage of completion
- βIt's always strictly better than a spinner, in every situation
Accessible Loading Announcements. A visual spinner alone tells sighted users something is loading, but a screen reader user gets no information at all unless the loading region uses aria-live="polite" (or role="status"), and aria-busy="true" while content is being replaced.
Mastery Achieved. You now have a real loading-state toolkit: spinners for quick operations, skeleton screens for reducing perceived wait time, honest progress bars for measurable operations, and accessible announcements via aria-live and role="status". Next, you'll learn Empty States β what to show when loading finishes but there's genuinely nothing to display.
Level Up π
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Use aria-live='polite', Not 'assertive', for Routine Loading
Polite live regions wait for a natural pause before announcing, avoiding interrupting a screen reader user mid-task β reserve assertive announcements for genuinely urgent updates, not routine loading states.
2Skeleton Screens Should Be Hidden from Screen Readers, Not Read Aloud Element by Element
Give a skeleton container aria-hidden='true' (paired with a text-based loading announcement elsewhere) so screen readers don't tediously narrate a series of meaningless placeholder shapes.
SEO Implications
- 1
Loading States Are Irrelevant to Crawlers If Content Is Server-Rendered
For SEO-critical content, prefer server-rendering the actual content over relying on a client-side loading state pattern, since crawlers generally don't wait for or interpret client-driven loading indicators the way a real user would.
Best Practices
Match the Loading Indicator to the Operation's Duration and Predictability
Use a spinner for quick, unpredictable-duration loads; a skeleton for layout-heavy content loads; and a real progress bar only when an accurate percentage is genuinely available.
Never Show a Loading Indicator for an Instant, Sub-100ms Operation
A flash of a spinner that immediately disappears feels more jarring than useful β consider a brief delay before showing a loading indicator for very fast operations to avoid this flicker.
Frequent Bugs
A loading spinner flickers on screen for a fraction of a second even for fast responses, creating a jarring flash.
Add a short delay (e.g. 200ms) before showing the loading indicator, only displaying it if the operation is still pending after that delay β this avoids the flash for genuinely fast responses while still showing a spinner for slower ones.
Screen reader users report no indication that a page section is loading.
The loading UI lacks proper ARIA β add role='status' and aria-live='polite' to the loading container, and aria-busy='true' while content is being replaced.
Real-World Examples
A Skeleton Screen for a Social Feed
A social media feed's posts take a moment to load on a slow connection. Rendering a PostSkeleton component β gray rounded rectangles matching the shape of an avatar, name, and post text β for each expected post gives users an immediate sense of the page's structure, reducing perceived wait time compared to a single centered spinner.
function FeedSkeleton() {
return (
<div role="status" aria-live="polite" aria-label="Loading posts">
{[1, 2, 3].map(i => <PostSkeleton key={i} />)}
</div>
);
}