Skeleton loaders (gray boxes shaped like the eventual content) make slow apps feel faster. They appear in nearly every modern app — Facebook popularized them; Airbnb, Linear, and others made them ubiquitous. The interview tests whether you understand the patterns and the subtle UX considerations.
Why skeletons
Compared to a spinner:
- Communicates that content will appear (not just “loading”)
- Reserves space — no layout shift when content loads
- Feels faster (perceived performance research)
When to use
- Initial page load
- Loading new data after navigation
- Pagination loading more items
- NOT for fast async (under 200ms — flash is annoying)
Implementation
Build a skeleton component matching the eventual layout:
function CardSkeleton() {
return (
<div className="card">
<div className="skeleton" style={{ height: 200 }} />
<div className="skeleton" style={{ width: '80%', height: 20, marginTop: 12 }} />
<div className="skeleton" style={{ width: '60%', height: 16, marginTop: 8 }} />
</div>
);
}
Animation
Two common animations:
- Shimmer: gradient sweeping across (most popular)
- Pulse: opacity oscillates
CSS-only:
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
Match the layout
The skeleton should occupy the same space as the eventual content. When data loads, no layout shift.
For variable content (text of unknown length): use placeholder lines of typical length.
Number of skeleton items
For lists: render the typical number of items (e.g., 6 card skeletons for a grid that usually shows 6).
Don’t render 100 skeletons for a 100-item list. Match what the user will see in the first viewport.
Avoiding flash
If data loads in under 200ms, skeleton flashes briefly and looks broken. Strategies:
- Delay skeleton render by 100ms; data may resolve before skeleton shows
- If skeleton is visible, keep it visible for at least 300ms (avoid flash)
This is “min loading time” pattern. Counter-intuitive but improves perceived experience.
Skeleton + suspense
React Suspense pairs naturally with skeletons:
<Suspense fallback={<CardSkeleton />}>
<Card />
</Suspense>
Skeleton vs spinner
- Skeleton for content with known shape (cards, lists, articles)
- Spinner for short async operations or unknown content
- Progress bar for measurable progress (uploads, downloads)
Theme considerations
- Light theme: gray (#e0e0e0 base, #f0f0f0 highlight)
- Dark theme: appropriate dark grays
- Reduce contrast slightly to avoid being more prominent than content
prefers-reduced-motion
Respect user preferences:
@media (prefers-reduced-motion: reduce) {
.skeleton { animation: none; }
}
Common antipatterns
- Skeleton that does not match final layout (jarring shift on load)
- Skeleton stays after content arrives (broken state machine)
- Skeleton too detailed (looks like real content, confusing)
- Animation too aggressive (distracting)
Frequently Asked Questions
Should every loading state have a skeleton?
No. Skeletons make sense for full-page or large-section loads. Inline async (button click → result) is fine with a small spinner.
How do I handle infinite scroll with skeletons?
Render a few skeleton cards at the bottom while loading the next page. Replace with real content as it arrives.
Are skeletons accessible?
Add aria-busy=”true” on the loading container. aria-live region announces when content arrives.