css
Skeleton Screens vs. Spinners: What Loading States Actually Do to Perceived Speed
Skeletons are supposed to feel faster than spinners. The research is more mixed than the listicles admit. What each pattern is for, when to show nothing at all, and how to build a skeleton that does not backfire.
Somewhere around 2018, “replace your spinners with skeleton screens” hardened into frontend folklore: skeletons feel faster, spinners make users watch the clock, case closed. The inconvenient part is that the best-known controlled experiment on this question found the skeleton screen performing worse than a spinner on every metric it measured. The pattern isn’t bad — but the reason to use it isn’t the one you’ve been told, and using it badly makes your app feel slower, not faster.
Here’s what each pattern actually is, what the evidence actually says, and a decision table you can defend.
The patterns
A spinner is an indeterminate progress indicator: an animation that says “something is happening” and nothing else. Not how much is done, not how long is left, not what’s coming. It’s honest about its own ignorance, which is both its virtue and its problem — a spinner explicitly announces you are now waiting.
A skeleton screen is a placeholder that previews the page’s structure: gray blocks where the headline, avatar, and paragraphs will land, usually with a shimmer or pulse animation. The term comes from Luke Wroblewski, in a September 2013 post titled “Mobile Design Details: Avoid The Spinner.” Working on the Polar polling app, his team found that adding spinners made users complain more about slowness — the indicator drew attention to the wait itself. Skeletons were his fix: focus the user on progress toward content rather than on the fact of waiting.
A determinate progress bar shows actual completion: 40% uploaded, 3 of 12 files processed. It requires knowing how much work remains, which is why it’s rarer — but when you can compute it, it beats both patterns above for anything long.
Nothing is also a loading state, and for fast operations it’s the correct one. More on that below, because it’s the most commonly botched.
What the research actually says
This is where this post parts ways with most of what ranks for “skeleton screen vs spinner.” The honest summary: the evidence is mixed, the effects are small, and the strongest claims in circulation trace back to a designer’s blog post, not a controlled study.
Wroblewski’s 2013 argument was observational — real user feedback from one app, no controlled comparison. Persuasive, but anecdote.
The best-known controlled test cuts the other way. In 2017, Viget ran an experiment (“A Bone to Pick with Skeleton Screens”) with 136 participants who waited identical durations behind a spinner, a skeleton, or a blank screen. The skeleton lost across the board: fewer skeleton users agreed the content “loaded quickly” (59% vs. 74% for the spinner), their estimated wait was longer (2.82s vs. 2.41s), and they even completed the follow-up task more slowly. The authors were appropriately careful — small study, one interface, short waits — and floated explanations: skeletons may work better in familiar interfaces users have seen load before, and may need genuinely short waits to feel like imminent content rather than a broken page.
Two 2018 studies lean back the other way, gently. Bill Chung ran two rounds of testing (80 in-person mobile users, then 80 remote participants) and found skeletons modestly ahead of spinners and blank screens on perceived duration — with the caveat that the advantage was minor, and that most production “skeletons” are really static splash screens rather than progressively filling structure. His animation sub-tests, at 20 participants each, are directional at best but consistent: wave/shimmer beat pulse, slow steady motion beat fast, and left-to-right beat right-to-left. Separately, Mejtoft, Långström, and Söderström (ECCE 2018) tested a fictional news site and found the skeleton version scored higher on average for perceived speed and ease of navigation — while first-time visitors using the spinner version actually found the target article faster.
Add it up and you get a defensible, unexciting conclusion:
- No one has demonstrated a large, reliable perceived-speed win for either pattern. Every study here is small; results point in both directions.
- Context seems to matter more than the pattern. Familiar layouts, short waits, and progressive filling favor skeletons; unfamiliar interfaces and longer waits favor plain indicators.
- The strongest argument for skeletons isn’t psychological, it’s structural. A skeleton that matches the final layout reserves space, so content doesn’t jump when it arrives. That’s a concrete, measurable win — it’s Cumulative Layout Shift, and it’s one of the Core Web Vitals Google actually scores you on. You don’t need a perception study to justify not shoving the page around.
If a blog post tells you skeletons “reduce perceived load time by 30%” or similar, ask where the number came from. We couldn’t find a credible source for any figure like that, so this post doesn’t contain one.
The decision table
Pattern choice should follow expected wait duration. The useful thresholds descend from Jakob Nielsen’s long-standing response-time limits — roughly 0.1 seconds to feel instant, 1 second to keep a user’s flow unbroken, 10 seconds to keep their attention at all — which he traces back to human-factors work from the 1960s. Mapped onto loading UI:
| Expected wait | What to show | Why |
|---|---|---|
| Under ~100ms | Nothing | It reads as instant. Any indicator is pure noise. |
| ~100ms–1s | Nothing, or a delayed indicator | Flow survives a sub-second pause. An indicator that flashes for 200ms makes the app feel jankier, not faster. |
| ~1–10s | Skeleton (familiar, content-shaped layouts) or spinner (actions, unfamiliar screens) | User needs confirmation the system heard them. Indeterminate is acceptable here. |
| Over ~10s | Determinate progress + what’s happening + a cancel affordance | Indeterminate animation past 10 seconds reads as “hung.” Show percentage, step names, or a count — anything that visibly advances. |
Two rules sit on top of the table:
Delay the indicator. The “flash of spinner” — indicator appears for 150ms, vanishes, content pops in — is worse than either pattern done properly, because a thing that flickers on and off reads as a glitch. The standard fix is to wait roughly 300–500ms before showing any loading UI at all: fast responses then render no loading state, and only genuinely slow ones get one. Pair it with a minimum display time (say, 300ms once shown) so the indicator that does appear never flickers.
Prefer determinate whenever you can compute it. “Step 2 of 4” or a real percentage beats any amount of shimmer. Skeletons and spinners are what you use when you genuinely can’t know progress — they’re the fallback, not the goal.
Doing skeletons right
If you’ve decided a skeleton fits — content-shaped page, familiar layout, wait in the 1–10s band — the implementation details are where the pattern wins or backfires.
Match the real layout, or don’t bother. A skeleton that reserves the wrong heights gives you the worst of both worlds: users get a preview that lies, and the page still shifts on arrival, which costs you CLS. Measure the real rendered components and mirror them — same card dimensions, same line counts, same image aspect ratios. A generic “three gray bars” dropped into every container is a splash screen wearing a skeleton costume.
Fill progressively if you can. Chung’s distinction is worth internalizing: a skeleton where the header resolves, then the images, then the text shows progress. One that sits complete and shimmering until everything swaps at once is functionally a spinner with better posture.
Animate calmly, left to right. The small-sample animation findings all point the same way: a slow wave sweeping left-to-right feels shorter than a fast one or a pulse. Mechanically, that shimmer is just an animated gradient sweeping across the placeholder — the same technique as any animated gradient background, scoped to a gray box.
Never skeleton forever. A skeleton has no failure mode of its own — if the request dies and nothing replaces it, the user stares at shimmering bones indefinitely, and that’s strictly worse than an error. Every skeleton needs a timeout that resolves to a real error state with a retry action. If you’re still shimmering at 10–15 seconds, you’re past the band where the pattern works anyway; say something true instead.
Implementation notes
The CSS is small. Pulse is an opacity oscillation; the wave is a translated gradient overlay:
.skeleton {
background: #e2e5e9;
border-radius: 4px;
animation: skeleton-pulse 1.8s ease-in-out infinite;
}
@keyframes skeleton-pulse {
50% { opacity: 0.55; }
}
/* Wave variant: a gradient sweeping left to right */
.skeleton--wave {
position: relative;
overflow: hidden;
animation: none;
}
.skeleton--wave::after {
content: '';
position: absolute;
inset: 0;
transform: translateX(-100%);
background: linear-gradient(90deg,
transparent, rgba(255 255 255 / 0.45), transparent);
animation: skeleton-wave 2s infinite;
}
@keyframes skeleton-wave {
100% { transform: translateX(100%); }
}
@media (prefers-reduced-motion: reduce) {
.skeleton,
.skeleton--wave::after {
animation: none;
}
}
Three details that earn their place:
prefers-reduced-motionis not optional. An infinite shimmer is exactly the class of ambient motion that vestibular-disorder users enable that setting to avoid. A static gray placeholder still communicates “loading” fine.- Keep the animation slow — around 1.5–2s per cycle. The frantic 0.8s shimmer you see in the wild is the variant that tested worse.
- Mark the region for assistive tech. A skeleton is visual-only; screen readers see nothing happening. Put
aria-busy="true"on the loading region and clear it when content lands, or use a live region to announce completion.
For the spinner side, the same reduced-motion rule applies, and honor the delay rule from the table: mount the spinner after a 300–500ms timer, not on request start.
The actual advice
Stop asking “skeleton or spinner” as a matter of taste. Estimate the wait, then read the table: nothing under a second (with a delayed fallback), content-shaped skeletons for familiar layouts in the 1–10s band, spinners for actions and unfamiliar screens, determinate progress the moment you can compute it, and a real error state before anyone’s patience runs out. The perceived-speed delta between the two patterns is small and contested; the layout-shift delta, the flash-of-spinner jank, and the skeleton-that-never-ends are large and entirely yours to control.
Try it
- CSS Skeleton Loader Generator — text, avatar, card, list, and table skeleton layouts with shimmer or pulse; tune colors and speed, copy the CSS and HTML. Runs entirely in your browser.
- CSS Loading Spinner Generator — 16 pure-CSS spinners (ring, dots, bars, ripple, orbit, and more) with color, size, and speed controls. No libraries, no GIFs.
- Animated Gradient Background — the same moving-gradient technique behind the shimmer, as full backgrounds: shifting linears, rotating conics, aurora blobs.