---
title: Loading Skeleton
slug: loading-skeleton
kind: pattern
summary: Grey placeholder shapes in the layout the real content will occupy, so a slow screen shows what is coming instead of a spinner or a blank.
problem: >-
  A screen takes a second and a half to load and shows nothing, or a spinner in
  the middle of an empty page. The person cannot tell whether it is working,
  broken, or empty — and when the content lands, the whole layout jumps.
family: [recover, orient]
data_shape: [collection, record]
principles: [orientation, progressive-disclosure]
interaction: [feedback]
density: low
complexity: low
status: stable
visibility: public
use_when:
  - Loading reliably takes longer than about 300ms and less than a few seconds.
  - The shape of the incoming content is known before it arrives.
  - The content is the main point of the screen, not a small part of it.
avoid_when:
  - Loading is usually under ~300ms. A skeleton that flashes is worse than nothing.
  - The wait is long or unbounded — that needs progress and an explanation, not a placeholder.
  - You do not know the shape, so the skeleton would lie about what is coming.
  - The result may be EMPTY. A skeleton promises content; deliver an empty state instead of pretending.
alternatives:
  - slug: empty-state
    when: There is genuinely nothing to load, which must never look like loading.
  - slug: optimistic-update
    when: The wait follows a user action and the outcome is predictable.
ask_leo: |
  Replace the loading spinner on this screen with skeleton placeholders.

  - Draw the skeleton in the same layout, at the same sizes and positions, as the
    real content. When the data lands, nothing should move.
  - Show it only after a short delay — around 200 to 300ms — so a fast response
    never produces a flash of placeholder.
  - Use neutral grey blocks with a gentle shimmer. Do not use fake text, fake
    numbers or a spinner on top.
  - Match the number of skeleton rows to a typical page, not to the maximum.
  - Mark the region as busy for assistive technology, and announce when the
    content has arrived.
  - Respect the reduced-motion preference by dropping the shimmer and showing
    static blocks.
  - If the request fails, replace the skeleton with a real error and a retry.
    A skeleton that never resolves is the worst outcome — it promises content
    forever.
related:
  - title: Empty State
    url: /patterns/empty-state
    summary: What to show when the content arrives and there is none — never the same thing.
---

## Anatomy

```
  loading                        loaded
  ┌──────────────────────┐       ┌──────────────────────┐
  │ ▓▓▓▓▓▓▓▓▓▓  ▓▓▓▓     │       │ Riverside Fit-Out    │
  │ ▓▓▓▓▓▓  ▓▓▓▓▓▓▓  ▓▓▓ │  ───▶ │ Dana · $8,400 · 12d  │
  │ ▓▓▓▓▓▓▓▓▓  ▓▓▓▓      │       │ Kestrel Joinery      │
  └──────────────────────┘       └──────────────────────┘
     same positions, same sizes — nothing moves on arrival
```

- **Same geometry as the real thing.** If the layout shifts when content
  arrives, the skeleton has failed at the one job a spinner cannot do.
- **A delay before appearing.** Under ~300ms a skeleton is a flicker, which
  reads as a glitch.
- **No fake content.** Grey blocks, never placeholder words or numbers — people
  read them, and briefly believe them.
- **A real terminal state.** It must resolve to content, an empty state, or an
  error. Never to itself.

## Why it works

A spinner says *something is happening*. A skeleton says *this specific thing is
happening, here, and it will look like this*. That converts an ambiguous wait
into an oriented one, and orientation is what makes waiting tolerable.

It also removes layout shift, which is a real cost rather than a cosmetic one:
content that jumps causes mis-clicks and forces the eye to re-find its place.

The perceived-speed effect is genuine but secondary. The reason to prefer a
skeleton is that it is *honest about structure* — it shows the shape of what is
coming rather than an abstract indicator of activity.

## The three states it must not confuse

This is the pattern's real risk. **Loading, empty and broken must look
different.**

- A skeleton that never resolves reads as loading forever, and people wait
  instead of reporting.
- A skeleton replaced by nothing reads as breakage.
- An [empty state](/patterns/empty-state) shown during loading tells someone
  their data is gone.

Decide all three renders up front. Most implementations build only the first.

## Getting it wrong

- **Flashing** on fast responses, because there is no delay before showing.
- **A skeleton that does not match the layout**, so everything jumps on arrival.
- **Fake text or numbers**, briefly read as real.
- **Skeleton plus spinner**, which is two answers to one question.
- **Shimmer with no reduced-motion fallback**, which is a genuine accessibility
  problem for a decorative effect.
- **No failure path**, leaving a grey page forever when the request errors.

## Exemplars

**Facebook** popularised the pattern, and the reason it stuck is the geometry
match: the skeleton is the post layout, so the feed never jumps.

**LinkedIn and Slack** both delay the skeleton, which is the detail most
implementations skip and the one that separates "feels fast" from "feels
glitchy".

**GitHub** is a good counter-example of restraint: most navigations are fast
enough that no placeholder appears at all, which is the correct choice when the
wait is short.

The extractable rule: **a skeleton is a promise about layout.** Keep the
promise — same shapes, same places — or you have added a flicker and a jump to
a screen that was merely slow.
