Skip to content

Skeleton loader

In review
Live · web
Loading content…

Primitive shapes: block, avatar, thumb, button, chip

Composed skeletons: same footprint as the real component

State: skeleton · motion shimmer. Toggle it; the footprint holds, so the layout never jumps.

// Web: React, consuming @dizyx/nockerl-tokens. (Web is the laggard platform. Spec + live demo,
// not yet exported from @dizyx/nockerl-react; promotion is tracked.)
// Mimic the FOOTPRINT of what is loading, so nothing shifts when data lands.
// A skeleton is NOT a spinner and NOT a progress bar; it has the content's shape.
function SessionList({ loading, sessions }: Props) {
if (loading) {
return (
<div aria-busy aria-live="polite">
{Array.from({ length: 4 }).map((_, i) => (
<SessionRowSkeleton key={i} /> // avatar + two lines + trailing value
))}
</div>
);
}
return <>{sessions.map((s) => <SessionRow key={s.id} {...s} />)}</>;
}
// Primitive shapes compose any skeleton. `motion="pulse"` swaps the sweep for a
// calm opacity pulse; both freeze under prefers-reduced-motion.
<Skeleton shape="avatar" size={28} />
<Skeleton shape="thumb" radius="card" />
<SkeletonText lines={3} /> {/* the last line renders shorter */}
<Skeleton shape="button" motion="pulse" />
PropTypeDefaultDescription
shape'line' | 'avatar' | 'thumb' | 'button' | 'chip''line'The placeholder silhouette. Its radius matches the real thing it replaces: avatar/chip pill, thumb card radius, button control radius, line the track radius.
motion'shimmer' | 'pulse''shimmer'Animation style. shimmer sweeps a token highlight across the block (transform only); pulse animates opacity. Both freeze under prefers-reduced-motion.
widthnumber | stringBlock width (px or any CSS length). Lines accept percentages; the last line of a SkeletonText is rendered shorter automatically.
sizenumberConvenience for square shapes (avatar): sets both width and height.
radius'track' | 'control' | 'card' | 'pill'Override the shape's default radius token when composing a custom footprint.
linesnumber1On SkeletonText: how many stacked lines to render, with a consistent gap and a shorter final line.

The skeletons are presentational (aria-hidden). Wrap the loading region in one element with aria-busy + aria-live=“polite” so assistive tech hears a single “Loading” status, not every empty box.

Cross-platform drift (tracked for reconciliation)

Skeleton is the content-shaped loader, distinct from its three feedback siblings: the spinner (an indeterminate circular loader with no footprint), the progress-bar (determinate/indeterminate progress against a known total), and the empty-state (a resolved zero-data result). Reach for a skeleton only when you know the shape of what’s arriving and want the layout to stay put.

  • Designed, not transcribed. The system above is drawn originally from the laws rather than lifted off a native screen, and the loading regions it is meant to replace fall back to a circular indicator. Promoting it to a real core/ui Modifier.skeleton() is the open work.
  • The Swift primitive is system redaction. .redacted(reason: .placeholder) greeks the real view, so the footprint is automatically exact, but it does not shimmer. Android/web add the travelling highlight; Swift’s placeholder is static unless a custom overlay is added. One shimmer expression needs to win.
  • Block radius. Web/Compose match each block’s radius to the component it replaces (avatar pill, thumb card, line track); Swift’s redaction inherits the view’s own shape. Behaviour converges; the source differs.