Skeleton Screens vs Spinners vs Nothing: What Actually Feels Faster in 2026
Skeletons aren't a free upgrade over spinners, and sometimes showing nothing is the right call. Here's how to pick a loading pattern that matches the actual perceived latency.

Somewhere around 2019, skeleton screens went from a Facebook-shaped curiosity to the default answer for "what do we show while this loads?" Six years later, we're still slapping them on everything — including requests that resolve in 180ms and would have been better off showing nothing at all. The honest answer is that skeletons, spinners, and empty frames each have a job, and picking the wrong one makes your app feel slower than it is.
The three loaders, honestly described
Before we get into when to use what, let's be precise about what each pattern actually communicates.
Spinners say: "something is happening, I don't know what shape it is or how long it'll take." They're honest about uncertainty but offer zero information about the result.
Skeleton screens say: "something specific is coming, and it will look roughly like this." They set expectations about layout and content density. The cost is that if the real content differs from the skeleton, you've just lied to the user and caused a layout shift.
Nothing — just the previous state or a blank frame — says: "this is fast enough that I'm not going to insult you by acknowledging the wait." Underused. Often correct.
There's a fourth option worth mentioning: optimistic UI, where you render the result immediately and reconcile on failure. It's the fastest-feeling pattern that exists, but it only works when the operation is likely to succeed and reversible on failure.
Latency buckets decide the pattern
The single biggest mistake we see is choosing a loader based on aesthetic preference rather than expected latency. Here's the rough framework we use on client projects:
Under ~200ms: show nothing
Human perception of "instant" tops out somewhere around 100–200ms. If your request reliably resolves below that, any loader you show will flash in and out faster than the eye can parse. That flash is worse than no loader — it reads as a glitch, not as feedback.
In our experience, this covers most cached reads, local state changes, and well-tuned API calls on fast networks. If you can measure p75 latency and it's under 200ms, just render the result when it arrives.
200ms – 1s: spinner, delayed
This is spinner territory, but with a twist: delay the spinner itself. Mount it after ~150–200ms so that fast responses never trigger it. Something like:
function useDelayedLoading(isLoading: boolean, delayMs = 200) {
const [showLoader, setShowLoader] = useState(false);
useEffect(() => {
if (!isLoading) {
setShowLoader(false);
return;
}
const t = setTimeout(() => setShowLoader(true), delayMs);
return () => clearTimeout(t);
}, [isLoading, delayMs]);
return showLoader;
}
This one hook eliminates the "spinner flash" problem on 80% of your mutations. Pair it with a minimum display duration (say 300ms) once the spinner does appear, so it doesn't blink out immediately on borderline requests.
1s – 10s: skeleton screens earn their keep
This is where skeletons genuinely win. The user is going to be waiting long enough to form expectations, and giving them a structural preview reduces the cognitive cost of parsing the result when it arrives. The content "snaps into place" rather than materialising from void.
The key constraint: the skeleton has to match the real layout. If your skeleton shows three equal cards and the real result is one hero card plus two smaller ones, you've caused a layout shift and broken the illusion.
Over 10s: progress, not placeholders
At this point the user needs to know how long is left and whether anything is actually happening. Determinate progress bars, step indicators, or streamed partial results beat any skeleton. If you can't give a percentage, give a log — showing "fetching inventory... reconciling prices... generating report" is infinitely better than a prettier spinner.
Where skeletons go wrong
Most broken skeleton implementations fail for the same handful of reasons.
The skeleton lies about the layout
You build a skeleton for a dashboard card that's 240px tall. The real card is 312px because the heading wrapped to two lines. Cumulative Layout Shift spikes, the page jumps, and the user's cursor is now hovering over the wrong button. The skeleton has made the experience actively worse than a spinner would have.
Fix: generate skeleton dimensions from the same tokens that drive your real components. If your card uses min-h-[20rem], your skeleton should too. Where content height is genuinely variable, use the median case and accept small shifts rather than designing for the extremes.
Skeletons on fast requests
If your p75 is 400ms, your skeleton is going to flash for a quarter-second and then disappear. That's not a loading state — that's a visual bug. Apply the same delayed-mount pattern you use for spinners.
Shimmer animations that fight the brand
The default shimmer gradient everyone copies from the first Tailwind tutorial they found is tuned for light backgrounds and medium contrast. On dark themes or high-contrast UIs it either disappears or looks like a seizure. Build shimmer as a token-driven component — base colour, highlight colour, duration — not a hardcoded gradient.
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton {
background: linear-gradient(
90deg,
var(--skeleton-base) 0%,
var(--skeleton-highlight) 50%,
var(--skeleton-base) 100%
);
background-size: 200% 100%;
animation: shimmer 1.6s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.skeleton { animation: none; }
}
That prefers-reduced-motion block is non-negotiable. Shimmer animations are among the most common accessibility complaints we hear from users with vestibular conditions.
Skeletons as decoration
If your skeleton has rounded avatars, three text lines, and a button-shaped rectangle, but the real component is a data table — you're not helping anyone. You're just performing "we care about loading states" without doing the work. A plain neutral block that matches the real component's bounding box is better than a decorative skeleton that misleads.
A decision tree you can actually use
When a teammate asks "should this have a skeleton?", here's the sequence we run:
- Is the operation under 200ms at p75? Show nothing.
- Is the resulting layout stable and predictable? If no, use a spinner — skeletons for variable layouts cause more harm than good.
- Will the user wait more than ~800ms? Skeleton is a good fit.
- Is this a mutation triggered by a user action (save, submit, delete)? Spinner on the triggering element, not a page-level skeleton. Users want feedback attached to the thing they clicked.
- Is this a navigation or page load? Skeleton for the above-the-fold region only. Don't skeleton the entire viewport — it looks like a broken page.
- Will it take more than 10 seconds? Show progress, not placeholders.
Measuring whether it worked
Perceived performance is subjective but not unmeasurable. The metrics we actually track:
- Time to meaningful content — not just when the request resolved, but when the user could reasonably start interacting. Skeletons shift this forward in perception even when the real TTFB hasn't moved.
- Interaction abandonment — how often users navigate away or re-click during the loading window. If this drops after introducing a skeleton, you've genuinely helped. If it doesn't move, the skeleton is theatre.
- CLS during loading transitions — if your skeleton-to-content transition causes shifts above 0.1, your skeleton is lying.
- Qualitative feedback — "the app feels snappy now" is a real signal, even if it isn't a number.
Don't ship a loading pattern change without picking at least one of these to watch. We've seen teams spend a sprint on "beautiful skeletons" with zero measurable impact because the underlying requests were already fast enough that users didn't need help.
Where we'd start
If you're auditing an existing product, instrument the latency of your top 10 loading states first. You'll almost certainly find three categories: things that are fast enough to need no loader, things that are slow enough to need a skeleton, and things where the real work is making the request itself faster rather than dressing up the wait. Fix the loader mismatches, then put real engineering time into the third category. A 2-second API call with a gorgeous skeleton is still a 2-second API call.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading

Empty States Are the Feature You Shipped Without Designing
Empty states are the first screen most users actually see. Here's how to design them so they teach, convert, and don't make your product feel broken on day one.

Focus Rings Are the Accessibility Win You're Still Botching
Focus rings are the cheapest accessibility win in your codebase, and most teams still remove them for aesthetic reasons. Here's how to design and ship them properly in 2026.
Hover States Are a Lie on Touch Devices: Designing for Both Without Breaking Either
Hover is a desktop metaphor we keep forcing onto phones. Here's how to design interactions that respect pointer type, hide information gracefully, and stop punishing touch users.
