Skip to main content

Design system components

Skeleton

Loading placeholder that mirrors the layout of incoming content.

Status: stable. Group: feedback. Markdown version: /design-system/components/skeleton.md

When to use

Skeleton holds the shape of content that is still loading so the layout never jumps: pick the variant that mirrors what will appear (text with a lines count, avatar, card, or free-form rect/circle sized via width/height) and compose several to sketch the incoming view. Each skeleton renders a polite role=status span whose visually hidden text is its label (by default the translated busyIndicator.loading string); pass decorative on all but one skeleton of a composed view, or on all of them when the surrounding component already announces loading. The shimmer is decorative and turns off under prefers-reduced-motion. Family rule: Skeleton for layouts, Spinner for sub-second unknown waits, Progress for measurable work.

When not to use

  • Do not use Skeleton for measurable work (uploads, multi-step jobs); that is Progress territory.
  • Do not leave skeletons on screen after content fails to load; resolve to an error state instead.
  • Do not use Skeleton for measurable work like uploads; Progress communicates completion, Skeleton only promises layout.

Look-alikes

  • EmptyState: Do not use EmptyState for errors or loading; AlertBanner owns failures and Skeleton owns loading.
  • Progress: Do not use Skeleton for measurable work (uploads, multi-step jobs); that is Progress territory.
  • Spinner: Do not scatter one spinner per element while a view loads; use a Skeleton layout or one region-level spinner.

Props

Skeleton props
PropTypeDefaultDescriptionRequired
variant"text" | "circle" | "rect" | "avatar" | "card"textShape variant. Defaults to text.No
widthnumber | stringnoneWidth (px or %)No
heightnumber | stringnoneHeight (px)No
linesnumber3Number of lines for text variant. Defaults to 3.No
classNamestring""Optional className.No
labelstringnoneVisually hidden text that names what is loading, read inside the status region. Defaults to the translated busyIndicator.loading string ("Loading"). Ignored when decorative.No
decorativebooleanfalseHide this skeleton from assistive tech (aria-hidden, no status region). Use it on every skeleton but one when several sketch one view, or when the surrounding component already announces loading. Defaults to false.No
animatebooleantrueAnimate shimmer (disabled with prefers-reduced-motion). Defaults to true.No

Variants

  • Available variants: text, circle, rect, avatar, card (default text).

Invalid combinations

The contract records no invalid combinations.

Keyboard

The contract records no keyboard interaction.

Rules

Every item under When not to use is a MUST NOT. MUST and SHOULD follow RFC 2119.

Skeleton rules
LevelRuleHow it is checked
MUSTARIA: Each non-decorative instance is a role=status span with aria-live=polite, named by aria-label, whose content is the same label as visually hidden text (default: the translated busyIndicator.loading string), so the label is live-region content and is readable in browse mode; whether a region inserted together with its text is announced depends on the screen reader.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: Decorative renders aria-hidden="true" with no role, live region or text; when several skeletons sketch one view, keep one labelled and mark the rest decorative so the view is one status, not many.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: Shimmer suppressed under prefers-reduced-motion.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTRequired stories produce no axe violations at the configured severity.Automated by npm run test:stories:matrix:ci. WCAG 2.5.8.
MUSTThe accessibility tree (role, name, state) matches the committed snapshot for every required story.Automated by npm run test:stories:matrix:ci. WCAG 1.3.1, 4.1.2.
MUSTThe component remains legible and operable under a real browser forced-colors: active pass.Automated by npm run test:stories:hc.
MUSTA human completed an end-to-end accessibility review of this component.Manual review, last on 2026-11-04.
SHOULDMirror the real layout; same widths, heights, and rhythm as the content that will replace it; so the swap is seamless.Not stated in the contract.
SHOULDName what is loading in the label prop ("Loading article", not "Loading content") for assistive tech.Not stated in the contract.
SHOULDCompose variants (avatar + text lines inside a card) to sketch whole modules; see the ComposedPage example.Not stated in the contract.
SHOULDGive one composed region one labelled status and wrap the skeletons inside it in an aria-hidden container; Skeleton has no decorative prop, so every bare instance is its own live region.Not stated in the contract.
SHOULD NOTDo not keep skeletons on screen when loading fails; resolve to an error state; a forever-shimmer reads as a hang.Not stated in the contract.

Install and import

  • Install: npm install @digitaltableteur/react (version 0.9.1, restricted access on npm).
  • Import: import { Skeleton } from "@digitaltableteur/react";
  • Entry point: @digitaltableteur/react/feedback exports it too.
  • Styles: import @digitaltableteur/react/style.css once at the app boundary.
  • Also available as the custom element <dt-skeleton>; see the custom elements manifest.