Skip to contentAsheeUIAsheeUI

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.
  • isBusy with a label exposes it as role="status" with aria-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.