Skeleton
The shape of content that has not arrived, drawn so the page holds still while it loads.
A skeleton is the shape of content that has not arrived. Its job is not to say "loading", a
spinner does that in a fraction of the room, but to teach the layout before the text
does: the heading is here, the avatar is here, three lines of body sit there. By the time
the content lands, the reader's eye already knows where to look, and the region does not
change size underneath them.
It is silent by default, and deliberately: aria-hidden on a placeholder that carries no
information is the honest markup. When the wait itself needs announcing, isBusy with a
label turns the same placeholder into a labelled busy status.
Usage
import { Skeleton } from "@asheeui/web";
<Skeleton className="h-4 w-64" />
The component has no size axis, because a placeholder is only ever the size of the thing it
stands in for. Sizing belongs in className, h-4 for a line of body text, h-6 for a
heading, h-28 for a picture, while radius and the shimmer resolve through the cascade.
Its surface is the theme's secondary token, so it follows the active theme rather than
carrying a grey of its own.
Shapes
A placeholder earns its name by matching the content. Round it like the thing it replaces,
sm for a line of text, full for an avatar, lg for a card, and compose the group out of
the same layout the real rows will use, so nothing shifts when they arrive.
A single grey bar is not a skeleton; it is a progress bar without the progress. The value is in the arrangement: an avatar beside two lines tells the reader that a person is coming, and a full-width block tells them a picture is.
Announcing the wait
isBusy changes what the placeholder is: it drops aria-hidden, becomes a role="status"
with aria-busy="true", and renders label in a visually hidden span so the status has
something to say. label defaults to Loading, which is rarely the most useful sentence,
name the thing that is loading.
One busy placeholder per group is enough. Each one is its own status region, so a group of
five placeholders marked isBusy announces the same sentence five times; mark the first and
leave its neighbours decorative. And because a live region reports a change rather than a
presence, a skeleton that is already on the page when the reader arrives announces nothing
until it changes, which is exactly what you want for first paint.
Props
radius is registered as sm and isAnimated as true, so both answer from the instance,
then from components.skeleton, then from the framework's fallback. As with the alert, the
registered radius means the application's defaultRadius does not reach a skeleton: a
product that rounds everything else still gets sm placeholders until it states
components.skeleton.radius. isBusy and label are instance-only, because they describe
this region's wait rather than the design.
The shimmer is applied through motion-safe, so a reader who has asked their system for
less motion sees a still placeholder with no script involved. isAnimated={false} turns it
off everywhere, for a product that would rather not animate a placeholder at all.
Accessibility
- By default the placeholder is
aria-hidden="true"with no role and no name: it stands in for content, and it is not content. Nothing about it is announced or reachable. isBusywith alabelexposes it asrole="status"witharia-busy="true", and the label is rendered as text inside the element rather than as an attribute, so the status has an accessible name the reader hears. Mark one placeholder in a group, not all of them.- Match the placeholder's size to the content that replaces it. The room it claims is the room the reader has already learned to expect; a region that grows when the data arrives undoes the point of the placeholder.
- The pulse is inside the reduced-motion-safe variant, so a reader who asked for less motion keeps the shape and loses the movement. The information is the layout, not the shimmer.
Skeletons, spinners and status regions make different amounts of noise on purpose; the reasons are collected in Announcements.