Skeleton states that know what’s coming

Skeleton states that know what’s coming
April 2026·5 min read

A skeleton is a promise. It says: something is coming, here’s roughly where it will land, stop scrolling away. Most products get this half right. They show a grey box, the real card arrives a different shape, the page jumps, and the user loses their place.

Three strategies for the same card. Same network delay, very different feel. Hit reload:

Same network delay. Three renderings.
Generic
Layout-matched
Optimistic

Dune: Part Three

2026 · Sci-fi

cached

Generic: A grey rectangle that admits it’s waiting. Cheap, safe, and the layout jumps when the real card lands.

Layout-matched: The skeleton is the shape of the card. Image block, title line, meta line, badge. When content arrives, nothing shifts.

Optimistic: No skeleton. The cached card renders instantly. A soft shimmer passes when the fresh data reconciles. The user never waits.

The generic card says “loading.” The matched card says “the image goes here, the title goes here, trust me.” The optimistic card doesn’t say anything, because there’s no waiting.

The problem isn’t the wait

Users don’t hate waiting. They hate not knowing what they’re waiting for, and they hate the page shifting under their finger when the wait ends.

A generic skeleton handles the first problem (yes, something is loading) and fails the second one (the real content is a different shape, so it pushes everything around it). That’s the layout shift. Google measures it as a core web vital for a reason: it’s the most disorienting thing a page can do.

The cheapest fix is to match the skeleton to the thing that replaces it. Same dimensions. Same blocks in the same places. When the real content arrives, it slides into a seat that was reserved for it.

Match the shape, then stagger

A skeleton with the same layout as the real card is good. A skeleton where each block pulses at a slightly different phase is better. A grid of ten identical skeletons pulsing in sync feels like a machine. A grid where each block is offset by 80ms feels like a page loading.

The numbers I reach for:

  • Pulse cycle: 1.4s.
  • Between blocks: 80ms stagger.
  • Opacity range: 0.45 to 1.0 (not 0 to 1, because 0 looks broken).
  • Colour: rgba(fg, 0.1), never pure grey. It should feel like a lighter version of the real card, not a stone slab.

Small detail: don’t animate skeletons if prefers-reduced-motion is set. A static skeleton still does the job and costs nothing.

Optimistic is the real win

The best skeleton is no skeleton. If the user has seen this card before, render the cached version immediately. When the fresh data lands, reconcile quietly. A soft shimmer, a crossfade, a subtle “updated” label. No wait.

On Filmflux I shipped a three-layer cache (memory, localStorage, IndexedDB) specifically so that returning users never saw a skeleton on the home screen. The home grid was eight cards, each normally a database read. Cached, it was zero. The most expensive feature on the app rendered instantly and then updated in the background.

The reason optimistic loading works isn’t speed. It’s that it respects that the user has been here before. A first-visit user gets a skeleton. A returning user gets their product.

When skeletons are the wrong answer

Skeletons are about shape. If there’s no shape to promise, skip them.

  • Loads under 300ms. A skeleton that flashes for 200ms and disappears is worse than nothing. The human eye registers the flash and not the relief. Use a short delay (150ms) before showing any skeleton at all.
  • Single-item loads with unknown shape. A search result where you don’t know if the card will be wide or tall. Use a spinner. A wrong-shape skeleton is worse than no skeleton.
  • Actions, not content. A button press waiting on a server response isn’t a skeleton problem. It’s a button-state problem. Spinner in the button, keep the rest of the UI frozen, done.
  • Infinite scroll. At the bottom of a list, a skeleton row is correct. At the top of a page, a skeleton is often replaceable with cached content.

The checklist

A skeleton is doing its job if:

  • Same shape as the real thing. Image block where the image goes. Title line where the title goes. No generic bars.
  • No layout shift on replace. Drop the skeleton into the real DOM, swap contents. Don’t insert and remove containers.
  • Respects reduced motion. Pulse off if the user asks.
  • 150ms delay before showing. If the data arrives fast, the skeleton never renders.
  • Cached content first when available. A skeleton should be a fallback, not a default.

The skeleton is a contract

Everything about the skeleton should be the promise of what’s arriving. Shape, size, position. When the promise is broken (the real content is a different shape), the user feels it even if they can’t name it. When the promise is kept, the page feels calm while it loads.

The one-line rule: a good skeleton is the real card with its paint turned off.