What it is
A skeleton is a grey shape that stands where content will be, in roughly the shape of the content, until it arrives. It tells the reader that something is coming and approximately what. It is the loading state for content whose shape you already know — a table’s rows, a card’s lines, a chart’s box. For work whose duration and outcome are unknown, use a Spinner. Nothing about this component is designed — see Open issues for how thoroughly that was checked and why it matters given adoption.Live example
Exports
Vocabulary
Say “skeleton” for one shape and “skeleton screen” for the assembly of them. Almost every real
use is the second: product code builds named compositions —
TabLoadingSkeleton, CallListItemSkeleton, FormConfigurationLoadingSkeleton — from many
instances of this one component.
Choose Skeleton when
- The shape of the incoming content is known before it arrives.
- The wait is long enough to notice and short enough to wait through — roughly a second to a few.
- The region is large enough that a blank space would read as a failure.
Choose something else when
Anatomy
One element per shape. A skeleton screen is many of them arranged to match the layout they stand in for — same widths, same line count, same spacing as the real content.Variants, sizes, and states
Four shapes:
Three animations:
pulse (the default), wave, and false for none.
Nothing chooses between them. pulse and wave are both available, the product uses both, and
no decision is recorded — TITAN-GAP-31. Until one is
chosen, use the default and stay consistent within a screen: two animations in one region read as
two different systems loading.
Size comes from width and height, or from the content it wraps. There is no size scale, so a
skeleton is sized against the thing it replaces — which is the point, and also why a skeleton screen
has to be maintained alongside the layout it mirrors.
Edge and failure states
Tokens
No tokens exist for this component. Its colour and animation come from the framework with no decision behind them, and arounded skeleton’s corner radius does not come from the
radius scale. That is the substance of
TITAN-GAP-31, and it means a skeleton is currently the
one grey in the product that no palette governs.
Composition
A skeleton screen mirrors the layout it replaces. Same number of lines, same widths, same spacing — built with the samespacing-* tokens as the real content, so the two are the same height
and nothing moves when the data lands.
Only the region that is loading. Page chrome stays: the header, the controls, and the footer are
already correct, and greying them out claims they are not.
In a table, the rows are the skeleton — column headers stay, because they are known. See
Table.
Do not animate a skeleton screen’s pieces independently. Many elements pulsing out of phase in
one region reads as noise; the framework’s animation is synchronised across instances by default,
and staggering it by hand is a decision nobody has asked for.
Product code builds these as named components — and three applications ship a TabLoadingSkeleton
with 21 instances each, which is the same file three times. If a skeleton screen is worth naming, it
is worth sharing; see Loading & skeletons.
Content
A skeleton carries no text. Never put words in one, and never label it “Loading…” — the shape is the message, and a label in a placeholder gets read as content.Accessibility
A skeleton is not a status message. It is decorative: it should be hidden from assistive technology, and the region should announce its loading state instead — abusy state on the region
that is loading, and an announcement when it finishes. A screen-reader user given a list of grey
boxes has been told nothing.
Announce completion, not the wait. The reader who cannot see the skeleton needs to know when the
content is ready, which is the announcement most loading states omit.
Reduced motion is unhandled. The animation does not respond to
prefers-reduced-motion, and the reader who has asked for less movement gets a pulsing region
anyway. Where a screen shows many skeletons at once, pass animation={false} under that preference
until the component handles it — TITAN-SKL-05.
A skeleton must not be the only indication a load failed. Something that never resolves reads as
a slow network, and a reader will wait far longer than they should before concluding otherwise.
Constraints
Known issues
Skeleton: open issues
Divergences, open decisions, and undocumented gaps for Skeleton.
Why it works this way
A skeleton buys patience by being specific. A spinner says “wait”; a skeleton says “wait, and here is what is coming” — which is why matching the real layout matters more than the animation does, and why using one for content of unknown shape gives up the only advantage it has. It is decorative on purpose. The temptation is to make it announce itself, since it is the most visible sign of loading. But the shapes carry no information, and a screen reader reading out a dozen placeholders is noise. The loading state belongs to the region; the skeleton is only how it looks.Status and changelog
Related
- Loading & skeletons — the pattern, and where the shared screens should live
- Spinner · ProgressBar — the other two waits
- EmptyState — what a skeleton resolves to when there is nothing
- Motion — the foundation this component does not use