Skeleton
A hand-drawn loading placeholder that pulses while content loads.
Overview
A Skeleton is a placeholder that stands in for content while it loads, preserving layout and
hinting at the shape of what’s coming. Prefer it over a bare Spinner
when you can approximate the final layout (a list row, a card, a paragraph).
Anatomy
A sketchy filled shape (@ghds/sketch-core) in the muted surface colour that gently pulses its
opacity. Three variants — rect (default), text (a rounded line), and circle (e.g. an avatar
placeholder) — set the default shape, radius, and size; width/height override them.
Variants & States
| State | Status | Notes |
|---|---|---|
| Pulsing | Implemented | Opacity animates between full and dimmed while loading. |
| Reduced motion | Implemented | The pulse is suppressed under prefers-reduced-motion; the placeholder is shown static. |
| Hover / Focus / Disabled | Not implemented | Skeleton is not interactive. |
Usage
Do
- Match the skeleton to the real content it replaces — same rough size and shape.
- Compose several (circle + text lines) to mirror a card or list row.
- Set aria-busy on the region that contains the skeletons so assistive tech knows it is loading.
Don't
- Don't leave skeletons up indefinitely — swap in real content (or an error state) when the request settles.
- Don't rely on the skeleton itself for a busy announcement — it is aria-hidden by design.
Accessibility
- All three platforms render the Skeleton as decorative and hidden from assistive tech
(
aria-hiddenon web;accessibilityElementsHidden/importantForAccessibilityon React Native). - Announce the loading state on the containing region instead — e.g.
aria-busy="true"on the card or list that the skeletons occupy. - Reduced motion: the pulse is suppressed under
prefers-reduced-motion(web) or the OS “reduce motion” setting (React Native). - See the Accessibility Guide.
Content
No content — a Skeleton is a shape only. Use width/height and multiple instances to approximate
the incoming layout.
Props API
React@ghds/react
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'rect' | 'text' | 'circle' | 'rect' | Placeholder shape. |
width | number | string | — | Width (number ⇒ px). Defaults to 100% (diameter for circle). |
height | number | string | — | Height (number ⇒ px). Defaults per variant. |
...rest | HTMLAttributes<HTMLSpanElement> | — | Native <span> attributes pass through. |
Web Components@ghds/web-components
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'rect' | 'text' | 'circle' | 'rect' | Placeholder shape (reflected attribute). |
width | string | — | CSS width. Defaults to 100% (diameter for circle). |
height | string | — | CSS height. Defaults per variant. |
React Native@ghds/react-native
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'rect' | 'text' | 'circle' | 'rect' | Placeholder shape. |
width | DimensionValue | — | Width. Defaults to 100% (diameter for circle). |
height | DimensionValue | — | Height. Defaults per variant. |
testID | string | — | Test handle for queries. |
Code examples
// React — @ghds/react
import { Skeleton } from '@ghds/react/skeleton';
<Skeleton variant="circle" />;
<Skeleton variant="text" width="70%" />;
<!-- Web Components — @ghds/web-components -->
<script type="module">
import '@ghds/web-components/skeleton';
</script>
<gh-skeleton variant="circle"></gh-skeleton>
<gh-skeleton variant="text" width="70%"></gh-skeleton>
// React Native — @ghds/react-native
import { Skeleton } from '@ghds/react-native/skeleton';
<Skeleton variant="text" width="70%" />;