Build a Skeleton Loading State System

Updated · techinterview.org

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 — the user hits the route and the shell renders instantly while data fetches. Show skeletons only for the parts that depend on the network; the static chrome (nav bar, header, footer) can render for real immediately.
  • Loading new data after navigation — switching tabs or opening a detail view triggers a fresh fetch. Keep the surrounding layout stable and swap only the changing region to a skeleton, so the transition doesn’t feel like a full page reload.
  • Pagination loading more items — when the user clicks “load more” or scrolls to the bottom, append a small batch of skeleton rows below the existing list. The content already on screen stays put; only the incoming slice shows placeholders.
  • 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.

newsletter

What's actually being asked right now

Interview patterns & comp trends, straight to your inbox.

No spam. Unsubscribe anytime.

newsletter

What's actually being asked right now

Interview patterns & comp trends, straight to your inbox.

No spam. Unsubscribe anytime.

1972 Soviet postage stamp commemorating the Mars 2 probe

worth a read

Mars For The Rest of Us — a weekly-or-more deep dive on the technical side of Mars exploration: rocket propulsion, microbiology, mission architecture, and everything in between. Written by Maciej Ceglowski.

Read it on Substack
Scroll to Top