A content-shaped placeholder for content whose layout is already known — it says what is about to appear and where. That is the whole difference from a Spinner, which says only that you are waiting, and from Progress, which says how much of a known total is done. Reach for a skeleton only when the shape is genuinely known: one that guesses wrong is a layout shift with extra steps.
An avatar, two heading lines, a media area, and a paragraph — the shape of the content that is coming, not a decorative arrangement. Each bar carries a --ds-skeleton-delay one step further on, so the group reads as a wave rather than one flashing slab. The frame is a presentation-only outline, not a Card: a demo of one component should not put another under test. The region carries one aria-busy and every bar is aria-hidden, so a screen reader hears it once rather than six times, and every width is the host's.
Each shape sets the one dimension a host cannot infer from its own layout, and nothing else. Width is always the host's, because only the host knows how wide the content it stands in for will be. So is the pulse's offset: --ds-skeleton-delay staggers a group into a wave, and unlike a shimmer sweep it needs no shared coordinate space.
| Example | Shape | Modifier | Sets | Usage |
|---|---|---|---|---|
| Text | --text | font.size.copy-md height | A line of copy — the default, and the overwhelming case | |
| Block | --block | size.control.lg floor, radius.control | Media, a chart, a card body; the floor only keeps it visible when a host forgets a height | |
| Circle | --circle | size.control.md box | An Avatar — its large box, since a placeholder avatar carries a whole row on its own |
The three divide by what the system actually knows. Picking by appearance rather than by knowledge is how a spinner ends up standing in for a known layout, or a skeleton for a request that may never return.
| Example | Component | Knows | Says | Usage |
|---|---|---|---|---|
| Spinner | Nothing | Wait | Duration and shape both unknown | |
| Progress | A total | This much is done | Measurable work — uploads, imports, batches | |
| Skeleton | The shape | This is coming, here | A known layout waiting on its data |
A skeleton is a promise about layout. Breaking it costs more than the spinner it replaced.
| Verdict | Rule |
|---|---|
| Do | Mark the loading region aria-busy="true" and every skeleton aria-hidden="true" — the region announces once, the bars never do. |
| Do | Match the skeleton to the content it replaces, not to a pleasing arrangement. Three bars where two lines will land is a promise the interface then breaks. |
| Don’t | Use a skeleton when you are guessing at the shape — that is a Spinner, which promises nothing about layout and so cannot break the promise. |
| Don’t | Leave a skeleton standing for something that may never arrive. A failed load is an EmptyState or an error; a skeleton pulsing forever is a hang with a nice texture. |
| Don’t | Animate a shimmer across a group at layer 1. Coordinating a sweep needs the skeletons to share a coordinate space, which is layout, which is layer 2. |