skeleton loading
why
loading fails in two directions: a spinner replaces the whole page and the layout convulses when data lands, or invented placeholder content promises a result the response may never deliver.
- reserving the ready data's exact geometry keeps data arrival a content change, not a layout change — eye and scroll position survive.
- refusing invented content keeps the pending state truthful, like every other state.
rule
a skeleton is the loading representation of data that will occupy a known position.
- reserve the ready data's exact visual geometry: container, column, row, image ratio, line count, spacing, action position.
- when data arrives, content replaces the skeleton; the screen must not become a materially different layout.
- a skeleton describes absence of data, not an imagined result: never invent names, counts, progress, statuses, avatars, activity, or actions the authoritative response has not supplied.
map data slots, not generic shapes
start from the ready component; name each independently loaded data slot and put the skeleton in precisely that slot. retain already-known layout: page shell, section heading, navigation, static controls, filters, actions whose availability does not depend on the pending result.
| ready data shape | loading representation | must stay stable |
|---|---|---|
| title text | one skeleton line with the title's expected width/height | heading position and surrounding spacing. |
| metadata line | corresponding short lines | row height and alignment. |
| fixed-ratio image | block with the same aspect-ratio | image frame and crop geometry. |
| fixed row list | the expected initial row count with each row's real grid | list width, row rhythm, and scrollbar behavior. |
| table or dense grid | header plus matching row/column skeletons | column widths and horizontal overflow boundary. |
| unknown/unbounded result | bounded neutral loading region | do not manufacture a long list or false count. |
- never skeleton static chrome, or replace the whole page with a centered spinner when only one data region is pending.
- a spinner is only for a genuine operation without known data geometry (a small action in progress); it is not a substitute for a data skeleton.
state boundary
- skeleton the first pending read before that data resolves.
- background refresh with prior authoritative data: keep it visible, optionally add a small truthful refresh affordance; never replace ready content with a skeleton.
- empty, denied, blocked, and failed are semantic states, not skeleton variants.
| state | representation |
|---|---|
| initial pending with known shape | skeleton in the exact data slots. |
| initial pending with unknown shape | bounded loading region or explicit progress only when the operation exposes real progress. |
| refreshing with prior data | retain prior data; optionally show non-disruptive refresh state. |
| empty response | owned empty state, never repeated skeleton rows. |
| failure or denied access | owned recovery/authorization state, never a skeleton. |
construction
- compose the ready layout first using real representative data dimensions.
- identify which dimensions come from pending facts and which are static.
- skeleton every pending slot using the ready slot's layout primitives and responsive rules.
- keep the same wrapper, grid, gaps,
aspect-ratio, and minimum dimensions between pending and ready branches. - keep skeleton motion subtle; respect reduced motion; expose the region's loading state to assistive tech without reading decorative blocks as content.
verification
- capture pending and ready states at every supported viewport.
- compare outer bounds, column widths, image ratio, row count, line height, scroll position — data arrival must not cause a large layout shift.
- throttle the query; confirm only the pending data region changes.
- refresh after ready data exists; confirm it stays visible.
- exercise empty, error, denied, blocked results — none may render a skeleton implying data is still coming.
- test reduced motion and a screen reader; decorative skeleton pieces must not become a noisy list of unlabeled elements.
good and bad
bad
if (isPending) {return <Spinner className="mx-auto mt-32" />;}
the whole page changes shape while one known data region loads: data arrival triggers layout convulsion, scroll position and reading focus are lost, and the page geometry is rebuilt twice for no benefit.
good
<WorkflowSummaryCard>{isPending ? <WorkflowSummarySkeleton /> : <WorkflowSummary value={data} />}</WorkflowSummaryCard>
the static card and its geometry remain; only the pending data slots are skeleton-reserved. content arrival swaps values into a stable frame with no layout shift, and nothing on screen invents an undelivered result.