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).

Highlight
Fig 1.1 · Skeleton · Designer view
Designer

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
Designer

Token usage per slot

container
spacing
  • gapspacing.tight
shape
radius
  • cornerradius.sm
color
  • backgroundcolor.surface.sunken
announcement
typography
  • sizetext.sm
Both

Figma ↔ Code property map

FigmaKindCodeNotes
VariantEnumvariantMaps text / circle / rect placeholder shape.
SizeEnumsizeMaps xs / sm / md / lg / xl. Bound to the eventual content's size.
AnimatedBooleananimatedToggles shimmer / wave animation. Always falls back to static under prefers-reduced-motion regardless of this prop.
MultipleBooleanmultipleWhen 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 AnnouncementTextannouncementNames what loads ("Loading messages"); rendered into the role=status sibling region.
Designer

Motion

TransitionDuration token
shimmermotion.duration.slower
pulsemotion.duration.base
Easing
motion.easing.standard
Reduced motion
Instant (jump cut)
Designer

Responsive behaviour

BreakpointChange
breakpoint.smBelow 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.
Both

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.

Both

Variants, properties, states

Variants

Structurally different versions of the component.

text circle rect

Properties

The same component, parameterised.

PropertyType
size xs | sm | md | lg | xl
animated boolean
multiple boolean

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
data
loadingcompleteerrorreduced-motion
Both

Figma↔Code mismatches

  1. 01
    Figma

    A static grey rectangle placeholder with no animation

    Code

    A skeleton with shimmer animation by default

    Consequence

    Designers 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.

    Correct

    Document 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).

  2. 02
    Figma

    One large skeleton block standing in for an entire card

    Code

    Multiple shape elements inside a container, each mirroring a content row

    Consequence

    Designers 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.

    Correct

    The 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.

  3. 03
    Figma

    Skeleton drawn without distinguishing the loading announcement

    Code

    Container has `aria-busy="true"` and a sibling `role="status"` text region

    Consequence

    Designers 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.

    Correct

    Add 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.

  4. 04
    Figma

    Skeleton with shimmer animation set as always-on

    Code

    Animation respects `prefers-reduced-motion` and falls back to a static placeholder

    Consequence

    The 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.

    Correct

    Document 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.

Both

Contracts

Non-negotiable contracts

  1. 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.

  2. 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.

  3. 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
Designer

Common mistakes

Blocker

#skeleton-no-aria-busy

Skeleton container without `aria-busy`

Problem

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.

Fix

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.

Major

#skeleton-no-reduced-motion

Shimmer animation runs unconditionally

Problem

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.

Fix

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.

Major

#skeleton-conflicting-loading-message

Multiple loading announcements compete

Problem

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.

Fix

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.

Major

#skeleton-shape-not-aria-hidden

Individual skeleton shapes read as graphics

Problem

Each shape in the container lacks `aria-hidden="true"`. SR users encounter N empty placeholders read as "graphic, graphic, graphic" instead of the single loading-announcement.

Fix

Set `aria-hidden="true"` on every individual skeleton shape. Only the container's `aria-busy` and the sibling `role="status"` announcement live in the AT tree; shapes are decorative.

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.