Designer view
Skeleton
A placeholder element that occupies the layout footprint of content not yet loaded. Distinct from a spinner: the skeleton preserves the exact dimensions of the eventual content, eliminating layout shift on load. Three variants cover the canonical placeholder shapes — text rows, circular avatars, and rectangular blocks.
Also called Loader Placeholder Shimmer
When to use
Use
For loading states where the eventual content has known dimensions and the wait is short enough that a layout-preserving placeholder reads better than a spinner. Common contexts: list rows, card grids, paragraph blocks, avatar groups, dashboard tiles. Skeleton tells the user "this content is coming, the layout will not jump".
Avoid
For loading states where progress is measurable — that is `Progress` (linear or circular determinate). For loading states where the eventual layout is unknown or genuinely empty — that is a centred spinner or an empty-state surface. For brief in-button or in-link loading — that is the host's loading state, not a skeleton overlay.
Versus related
- toast
`Toast` announces transient state changes after they complete; `Skeleton` reserves space during the wait. Skeletons disappear on load; toasts may follow as completion confirmations.
- progress
`Progress` reports the completion fraction (or busy state) of an ongoing task — measurable percentage via `aria-valuenow`. `Skeleton` reserves layout while content fetches — placeholder shapes mirror the final layout, no percentage. Choose Skeleton when the layout is the message ("the page will look like this"); choose Progress when the completion is the message ("we are 60% through"). A single load may use both — Skeleton in the data region, Progress in a header showing overall completion across multiple loads.
Skeleton is the canonical layout-preserving loading placeholder — a wireframe stand-in that occupies the exact footprint of content not yet loaded. Three variants (text, circle, rect) cover the canonical placeholder shapes; the shimmer or wave animation reinforces the in-flight signal but collapses under prefers-reduced-motion. Common hosts: list rows, card grids, avatar groups, paragraph blocks. The reference documents the aria-busy contract on the parent container, the aria-hidden-on-individual-skeletons rule, the role=status sibling for the textual loading announcement, and the divergence from Progress (use Progress when the percentage is known).
Figma anatomy
| Slot | Figma type | Hint |
|---|---|---|
container | frame | Auto-layout frame mirroring the eventual content's layout grid; aria-busy bound to the loading state |
shape | frame | Frame with placeholder fill; radius and aspect bound to variant; animation via Smart Animate or simply documented as "shimmer" |
announcement | text | Visually-hidden text (sr-only) describing what is loading |
Token usage per slot
container- spacing
- gap
spacing.tight
- gap
shape- radius
- corner
radius.sm
- corner
- color
- background
color.surface.sunken
- background
announcement- typography
- size
text.sm
- size
Figma ↔ Code property map
| Figma | Kind | Code | Notes |
|---|---|---|---|
Variant | Enum | variant | Maps text / circle / rect placeholder shape. |
Size | Enum | size | Maps xs / sm / md / lg / xl. Bound to the eventual content's size. |
Animated | Boolean | animated | Toggles shimmer / wave animation. Always falls back to static under prefers-reduced-motion regardless of this prop. |
Multiple | Boolean | multiple | When true, the skeleton stands in for a list of N items rather than a single shape; consumer drives the actual item count via children rendering. Renamed from `count` to avoid the boolean-named-as-number naming-trap. |
Loading Announcement | Text | announcement | Names what loads ("Loading messages"); rendered into the role=status sibling region. |
Motion
| Transition | Duration token |
|---|---|
shimmer | motion.duration.slower |
pulse | motion.duration.base |
Responsive behaviour
| Breakpoint | Change |
|---|---|
breakpoint.sm | Below this width, multi-shape skeleton containers collapse to a simpler layout (one or two shapes per row instead of three or four). The eventual content's responsive layout drives the skeleton's responsive layout — they share the same breakpoint reflow rules. |
Internationalisation
RTL · mirroring
Text-variant skeletons render direction-neutral (a grey rect reads identically in either direction). Multi-shape containers preserve the layout-direction of the eventual content via logical properties (`inset-inline-start` for leading shapes), so the skeleton mirrors when the content mirrors. Shimmer animation direction follows the writing direction (left-to-right in LTR, right-to-left in RTL).
Text expansion
The text-variant skeleton width should match the eventual text width — including the 30–50 % expansion for German and Romance languages. Reserve width via `min-width` mirroring the typical content length, allow growth to accommodate longer text. The loading announcement text expands like any UI string and follows the canonical i18n contract.
Variants, properties, states
Variants
Structurally different versions of the component.
text circle rect Properties
The same component, parameterised.
| Property | Type |
|---|---|
size | xs | sm | md | lg | xl |
animated | boolean |
multiple | boolean |
States
Browser/user-driven (interactive) vs. app-driven (data).
| Kind | States |
|---|---|
interactive | |
data | loadingcompleteerrorreduced-motion |
Figma↔Code mismatches
- 01 Figma
A static grey rectangle placeholder with no animation
CodeA skeleton with shimmer animation by default
ConsequenceDesigners ship a static grey block; developers ship an animated shimmer. The two visually disagree — designers assume "loading state" reads from the static placeholder; developers add motion that designers never reviewed.
CorrectDocument the animation as a canonical part of the skeleton anatomy in Figma — either via Smart Animate or a "Shimmer" prototype frame. Code matches the Figma motion spec. The animated property toggles motion off for environments where it is hostile (reduced-motion, embedded screenshots).
- 02 Figma
One large skeleton block standing in for an entire card
CodeMultiple shape elements inside a container, each mirroring a content row
ConsequenceDesigners draw one big grey rect; developers ship 4-8 separate skeleton shapes mirroring the eventual layout (avatar circle + two text rows + a rect). The two diverge visually and the user sees a less-faithful preview than the design intended.
CorrectThe Figma skeleton uses the same anatomy as the eventual content — one shape per layout row. The container holds multiple shapes, each sized to the content it stands in for. Design and code both express the layout preview at the same fidelity.
- 03 Figma
Skeleton drawn without distinguishing the loading announcement
CodeContainer has `aria-busy="true"` and a sibling `role="status"` text region
ConsequenceDesigners focus on the visual placeholder; developers wire AT semantics that the design file does not document. The visual passes review; the AT-side breaks because the announcement region is missing or names something generic ("Loading…") rather than the content.
CorrectAdd a "loading announcement" annotation in the Figma file naming what loads ("Loading messages", "Loading product list"). The annotation maps to the canonical announcement slot; both surfaces ship the meaningful announcement.
- 04 Figma
Skeleton with shimmer animation set as always-on
CodeAnimation respects `prefers-reduced-motion` and falls back to a static placeholder
ConsequenceThe Figma file ships shimmer-by-default with no reduced-motion variant; developers shipping reduced-motion-aware code remove the shimmer for users who need it. The two diverge under accessibility settings.
CorrectDocument the reduced-motion variant in Figma as a separate static frame. Code matches: animation runs by default, collapses to static under `prefers-reduced-motion: reduce`. The animated property in the canonical schema gates the same toggle.
Contracts
Non-negotiable contracts
APGWAI-ARIA aria-busy + status role patterns The skeleton container carries `aria-busy="true"` while loading is in-flight; individual skeleton shapes carry `aria-hidden="true"`. A sibling `role="status"` region announces the loading start textually.
Without `aria-busy`, SR users navigating into the region encounter placeholders read as graphics with no signal that content is loading. Without `aria-hidden` on shapes, each placeholder reads separately and floods the announcement queue. Without the status region, the textual announcement of what loads is missing entirely.
WCAGWCAG 2.3.3 — Animation from Interactions (AAA) + 2.2 user-controllable timing Shimmer / wave / pulse animation respects `prefers-reduced-motion: reduce` and falls back to a static placeholder. The motion is reinforcement, not the contract; users with motion sensitivity must be able to suppress it.
Continuous shimmer animation across multiple skeletons triggers vestibular discomfort and cognitive load for affected users; the layout-preserving promise of Skeleton becomes hostile when motion cannot be turned off. The reduced-motion fallback is a non-negotiable accessibility floor.
Canon The textual loading announcement names what loads, scoped to the region. "Loading messages", not "Loading…" or "Please wait". One announcement per loading region; page-level loaders handle page-level status separately.
Generic "Loading…" announcements give SR users no prediction of what arrives; multiple competing announcements ("Loading… Loading messages… Loading product list") confuse the user about which region actually completed. Region-scoped, content-named announcements give the user a model of the page.
Vocabulary drift
- Material 3
Loading indicator (Skeleton variant)- Material 3 documents Skeleton as a Loading-indicator sub-pattern alongside circular and linear progress; the canonical separation here treats Skeleton and Progress as distinct components.
- Polaris
SkeletonBodyText / SkeletonDisplayText / SkeletonThumbnail- Polaris ships variant-specific Skeleton components rather than one component with a variant prop; the composite contract matches but the surface granularity differs.
- Carbon
SkeletonText / SkeletonPlaceholder- Atlassian
Spinner / Loading- Atlassian historically uses a centred Spinner rather than layout-preserving Skeleton; production teams adopting the canonical Skeleton pattern lean on third-party libraries.
- Radix
Skeleton
Common mistakes
#skeleton-no-aria-busy
Skeleton container without `aria-busy`
The skeleton renders during loading but the container has no `aria-busy="true"`. SR users may navigate into the region and encounter placeholder shapes that read as graphics with no signal that the content is in flight.
Set `aria-busy="true"` on the skeleton container while loading. Remove it when the eventual content arrives. Pair with a sibling `role="status"` region carrying the textual loading announcement so SR users hear the loading start AND know the region is busy.
#skeleton-no-reduced-motion
Shimmer animation runs unconditionally
The skeleton's shimmer or wave animation runs regardless of the user's `prefers-reduced-motion` setting. Users with vestibular disorders or motion sensitivity see continuous motion they cannot suppress.
Wrap the animation in a `prefers-reduced-motion: no-preference` media query. Under `prefers-reduced-motion: reduce`, render a static placeholder. Document the reduced-motion fallback as part of the canonical anatomy.
#skeleton-conflicting-loading-message
Multiple loading announcements compete
The skeleton ships its own `role="status"` announcement, the page-level loader ships another, and a toast spawns a third. SR users hear "Loading… Loading messages… Page loading complete" in a chaotic order; the canonical signal is lost.
The skeleton's announcement is region-scoped — names what loads in this region only. The page-level loader, if any, announces page-level status. Toasts announce completion events, not loading start. Avoid duplicating announcements; one loading start per region, one completion announcement per resolution.
Accessibility hints
| Slot | Accessibility hint | |
|---|---|---|
container | Container carries `aria-busy="true"` while the loading is in-flight. The container's `role` matches the eventual content's role when known (`role="region"` for landmarks, `role="row"` for table rows). Pair with a sibling `role="status"` element that announces the loading start textually (visually-hidden if no visual loading text). | |
shape | Each individual skeleton is `aria-hidden="true"` so SR users do not encounter N empty placeholders read out as graphic-graphic-graphic. The container's `aria-busy` and the sibling `role="status"` carry the loading announcement for AT. | |
announcement | Wraps in `role="status"` with `aria-live="polite"`. The text describes what is loading ("Loading messages") and updates to a completion / error message when the load resolves. Avoid writing it as "Loading…" generically; name the content so SR users can predict what arrives. |