Skeleton
The shape of what is on its way, so the page does not jump when it arrives.
Try it
The card is the same height before and after: a text shape stands in the line it replaces.
Three shapes
There are no moulds — no skeleton of a card, of a table. A mould is a second drawing of a component that has to be kept in step with the first; three shapes put where the content will be are always in step, because they are in the content’s own layout.
A row of a table
Skeleton, loader or progress
| When | Use |
|---|---|
| The layout of what is coming is known: a card, a row, a record | Skeleton |
| Something is working and its shape is not known, or is small: a button, a search | Loader |
| How far along it is can be said | Progress |
| It takes under a third of a second | nothing: a flash of grey is worse than a wait |
The sweep
Each shape has its own sweep, 1.8 seconds from edge to edge, decided on 2026-09-21 against a pulse and against one band crossing every shape at once. The single band is a gradient as wide as the window held with background-attachment: fixed: it animates a painted property, and it falls apart inside a transformed ancestor and on iOS. The loop keeps its own timing and is not a motion token, as the Loader’s. Under dir="rtl" it runs the other way.
Reduced motion does not freeze it: a still grey bar is content, not loading. What is dropped is the travel — the band stays where it is and breathes, at half the pace.
The fill
It is the pressed wash and not a surface. surface/sunken is surface/base in dark: nothing on the page and a hole on a card. The wash is a state layer that composites over whatever is under it, in both modes, so one shape is right on the page, on a card and in a panel. The band is the same wash again, a denser passage, and needs no colour of its own. It is not held to a contrast ratio: a skeleton is not information, and what says “loading” to someone who cannot see it is aria-busy. It keeps a transparent border, which forced colours paint.
Accessibility
The shapes are aria-hidden: twenty of them would be twenty empty things. The region that is loading is the caller’s, and says aria-busy while it loads. They are spans, so one can stand inside the paragraph or the heading it replaces, and the outline of the page does not change when the content arrives.
Using it
import { Skeleton } from 'alpenglow';
// The region that is loading says so; the shapes say nothing.
<section aria-label="Patient" aria-busy={loading}>
{loading ? (
<>
<Skeleton variant="circle" size={40} />
<h3><Skeleton width="50%" /></h3>
<p><Skeleton lines={3} /></p>
</>
) : (
<Patient … />
)}
</section>| Prop | Type | Default |
|---|---|---|
variant | 'text' | 'circle' | 'rect' | 'text' |
lines | number — text only | 1 |
width | number | string | the whole width; the last of several lines, 60% |
height | number | string | text: the glyphs’ share of its line; rect: 40 |
size | number | string — circle: both | 40 |
className, style | — |