# 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 `
` is already full width, so height was the one dimension a caller could omit and get nothing for — a bare `` was a zero-height box, which is not a small skeleton but an absent one. Twelve pixels is the height `SkeletonLine` already chose, and any class the caller passes replaces it. Also accepts: `HTMLAttributes`. ## SkeletonLine A line of type. Height is the line's; width is how far it runs. Also accepts: `HTMLAttributes`. ## SkeletonBlock A block: an image mount, a plate, a chart. Also accepts: `HTMLAttributes`. ## SkeletonCircle A circle: an avatar, a marker. Also accepts: `HTMLAttributes`. ## SkeletonText A paragraph. The last line is short, because real prose ends mid-measure. A block of equal full-width bars reads as a table, and the reader then flinches when a paragraph arrives instead. ### Props - `lines` — `number` default `3`. How many lines of prose to stand in for. Also accepts: `HTMLAttributes`. ## SkeletonPage The frame a skeleton sits in: the live region, and the single pulse. One `animate-pulse` on the wrapper rather than one per part, so the page breathes together instead of shimmering out of phase — twenty independently animated bars is a visual noise floor, not a loading state. A skeleton whose shape does not match what replaces it is worse than none: it promises a layout and then the page jumps out from under the reader. Compose the parts to match the real thing, and change both together. ### Props - `label` (required) — `string`. What is loading, in words. Announced once; the shapes themselves are `aria-hidden`, so this sentence is the entire experience for a screen reader and "Loading" alone tells it nothing. - `children` (required) — `ReactNode`. Also accepts: `HTMLAttributes`. ## Example — record list ```tsx import { SkeletonBlock, SkeletonLine, SkeletonPage, SkeletonText } from '@misoto22/folio'
{[0, 1].map((row) => (
))}
``` ## Example — parts ```tsx import { SkeletonBlock, SkeletonCircle, SkeletonLine, SkeletonPage, SkeletonText } from '@misoto22/folio'
``` ## Example — shape match ```tsx import { Avatar, SkeletonBlock, SkeletonCircle, SkeletonLine, SkeletonPage, Text } from '@misoto22/folio'
While it loads
What lands
Henry Chen Rebased onto main and the typecheck is green again. Merging once CI finishes.
```