# Build a Skeleton Loading State System

Source: https://www.techinterview.org/post/3233475203/build-skeleton-loading-state-system/
Updated: 2026-07-26 · 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](/companies/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.
