Skeleton

A pulsing placeholder that holds the shape of content still loading, so the page does not jump when the data lands.

import from ~/components/ui/skeleton

Shapes

block
circle
text

shape='block' | 'circle' | 'text'

Pick the silhouette with the variant. A circular avatar placeholder no longer needs a rounded-full override, which is exactly what the variant was added for.

An avatar row

Loading members

the case the circle shape exists for

A round avatar beside two lines of text. Before shape='circle' this row had to be drawn with a square, because rounding it from the call site is a restyle.

The shapes we actually load with

Loading

a page's worth

Each stands in for a real piece of content: a heading, a paragraph, an avatar, a thumbnail.

A loading endeavor card

Loading endeavor

composed loading state

The placeholder mirrors the card it replaces, block for block, so nothing moves when the data lands.

A loading list

Loading endeavors

repeated rows

Load the number of rows you expect, not one. Three to five reads as a list; one reads as a bug.

Rules

Do

Mirror the shape of the real content so the layout does not jump when data arrives.
Hide the shapes from assistive tech with aria-hidden and announce the wait once with role="status".
Repeat rows for a list. Load four placeholder rows if four rows are coming.
Sizing classes are the contract here: h-3, w-2/3, size-9. That is what the component asks for.
Pick the silhouette with shape. A circle is a variant, never a rounded-full at the call site.

Don't

Do not let a screen reader read the boxes as content. An unlabelled grid of pulses says nothing.
Do not use it as a general spinner. An indeterminate wait with no known shape wants a loader.
Do not flash one for a request that usually resolves in under a moment. The flicker costs more than the wait.
Do not pass color, rounded or shadow classes. Sizing is layout and is fine. Shape is not.