# Skeleton The shape of the page, before the page. - Group: Feedback - Import: `import { Skeleton } from '@misoto22/folio'` - Page: https://ui.misoto22.com/components/skeleton/ - Related: spinner, empty-state ## When to reach for it A wait the reader would otherwise think was a broken page. A shape that describes what is coming beats a dot that describes nothing. ## Anatomy - **Frame** (required) — SkeletonPage: the div carrying role="status", aria-busy="true" and the single pulse. It is the only part that speaks and the only part that moves — the shapes inside it do neither. - **Label** (required) — The sr-only sentence inside the frame, from the required label prop. Every shape is aria-hidden, so this one sentence is the entire loading state for a screen reader. - **Fill** — Skeleton itself: a --stone rectangle one line tall — h-3 — and, being a div, already full width. Height is the one dimension the element does not have on its own, so that is the one the base supplies; anything in className replaces it. - **Line, Block and Circle** — The three presets over that fill. SkeletonLine is a 12px pill and takes its width from the caller, SkeletonBlock only sets --radius-sm, and SkeletonCircle is a fixed 36px round. - **Paragraph** — SkeletonText: three lines by default, the last at 62% so it ends mid-measure the way prose does rather than squaring off into a table. ## Best practices ### Do - Wrap the shapes in SkeletonPage even when there is only one bar. The role, the aria-busy and the pulse all live on the frame, so parts used loose are aria-hidden, silent and completely still — a grey rectangle that never resolves. - Give every Skeleton the height of the thing it stands in for. The base falls back to one line, so a block left unsized is no longer invisible — it is a 12px bar where a 160px plate is about to land, and the page jumps by the difference. - Make whatever replaces the skeleton announce itself or take focus. aria-busy never flips to false here — the frame is unmounted, not updated — so the end of the wait is the disappearance of the only thing that was speaking. ### Don’t - Do not add animate-pulse to a part. The frame already animates opacity and a second ramp on a child multiplies with it, so the part beats at a rate the shape around it does not. Reduced motion is not the reason it is wrong: the rule in keyframes.css is a universal floor on animation-duration and transition-duration, so a hand-written Tailwind animation is clamped along with everything else. data-folio-animated is a component asserting its motion is decorative, not the mechanism that does the cancelling. - Do not nest one SkeletonPage inside another. Each is a role="status" region with its own sr-only sentence, so a page assembled from two skeleton sections announces two loading messages and marks two regions busy for a single wait. - Do not carry a skeleton between screens unedited. SkeletonCircle is 36px and SkeletonLine is 12px tall whatever they stand in for, so a copied avatar mount and a copied heading are two guaranteed reflows the moment the real content lands. ## Accessibility - One live region on the wrapper; every shape inside it is aria-hidden. - One pulse on the wrapper, not one per bar, so the page breathes together. ## Skeleton The fill every skeleton part is drawn from: `--stone`, at the height of one line until something says otherwise. Deliberately NOT a shimmer. A shimmer sweeps a highlight across the shape, which implies a light source; the White Reset has none, so the sweep reads as a bug rather than as loading. The whole page breathes together instead — see `SkeletonPage`, which owns the single pulse. `h-3` is a default rather than a design. A `