Skip to content
HyperDesign System

Skeleton

A pulsing tint block that holds the place of content still loading, so the layout doesn’t jump when it arrives. It reads on the canvas and on cards in both themes. Source: src/components/ui/skeleton.tsx.

Shapes

Skeleton is one block with no shape presets. Match the height of the text it replaces (16px for 14px text, 12px for a caption) and the radius of the element.

A list of rows

Mirror the real row: avatar, a name line, a preview line and a timestamp. Vary the line widths so the block reads as text, not a bar chart.

Loading threads…

A card

Inside a real Card the shell renders at once and only the content waits. The tile, title and description keep the sizes of the loaded card.

Loading integration…

Props

Skeleton props
PropTypeDefaultDescription
classNamestring—Merged last. Set the height, width and radius here; the base is rounded-md bg-tint-10 with the animate-skeleton pulse (1.2s, infinite).
...propsComponentProps<"div">—Native attributes pass through. No variants, sizes or shape presets.

Best practices

When to use

  • Content with a known shape that takes more than a few hundred milliseconds: lists, cards, a profile header.
  • Not for actions. A pending button needs a loading state on the button (not built yet), not a skeleton.

Behavior

  • Swap skeleton for content in one step, at the same size, so nothing below moves.
  • Show the number of rows you expect, or three when you don't know. A screen of twenty grey bars reads as broken.
  • Under reduced motion the global rule stops the pulse after one cycle, leaving a static tint.

Accessibility

  • Skeletons are silent. Put aria-busy="true" on the region and one sr-only line such as "Loading threads…".
  • Remove aria-busy when the content lands so the region is announced once, complete.

Content

  • Copy the real layout's dimensions, radius and gaps. A skeleton that doesn't match causes the jump it was meant to prevent.