Components · Feedback
Pulsing grey stand-ins that hold a region’s shape while its content loads, so the layout doesn’t jump when data arrives. <Skeleton> is one block you size yourself; <SkeletonText> stacks N lines and shortens the last to read as prose. Both are aria-hidden — purely visual — so the loading region carries aria-busy and a real status message elsewhere. The base pulse is enriched here with a subtle gradient sheen sweeping across each block. When a request has no shape to hold, fall back to an inline spinner.
avatar · 40×40 · pillline · h16card / image block · h96 · rounded-lgSkeletonText · lines=4 — equal h16 lines, gap-8; the final line is 66% wide so the stack reads like the tail of a paragraph.
Every block is the same primitive: --surface-3 fill, rounded, an opacity pulse and a left-to-right sheen. You size and shape it to the thing it stands in for — a circle for an avatar, a short bar for a label, a tall rounded block for a card. Nothing is announced to assistive tech (aria-hidden).
A team card loading — chip + title line, then SkeletonText lines=3, laid on surface-2.
Roster rows mirror the real avatar · name/meta · tier badge shape — so the list settles in place, not reflows, when the volunteers arrive.
When there’s no layout to preserve (a one-shot fetch, a button submit) reach for the spinner instead — the Loader2 ring, spinning. A loading button keeps its variant color and dims only slightly (opacity .9, cursor:wait) so it reads as working, not disabled.
animate-pulse rounded-md bg-surface-3. Size + shape with your own h-* / w-* / rounded-*. Here a gradient sheen sweeps across on top of the pulse.lines (default 3) — that many h-4 lines, gap-8; the last is w-2/3. Constrain the stack’s width via className.aria-hidden — visual only. Announce the load on the region with aria-busy + a role="status" label, not on the blocks.Loader2 ring (animate-spin) for work with no shape to hold. In a loading button it keeps the variant color and dims slightly — “working”, not disabled.prefers-reduced-motion stills the shimmer and slows the spinner — the placeholder still reads without the sweep.Source · Skeleton.tsx (Skeleton + SkeletonText, lines prop, animate-pulse rounded-md bg-surface-3) and Button.tsx loading state (Loader2 animate-spin, keeps variant color) · design-system/feedback.