Components

Skeleton

A pulsing placeholder that reserves layout space while content loads, reducing perceived latency and preventing layout shift.

Basic usage

Compose blocks of different sizes to mirror the shape of the content they stand in for. Size and rounding are controlled entirely through className.

Card placeholder

Stack skeletons to outline an entire component — here a media card with an image, title, body lines, and an avatar row.

Shapes

Use rounded-full for avatars and chips, rounded-lg for thumbnails, and a slim height with rounded-full for text lines.

Surface token

The block is filled with --muted, the neutral grey step — not --accent. Accent is the light step of the brand: on the light palette it is a cream yellow, so a whole loading page would bleed gold, in exactly the colour Switch and Tabs use to mean “this one is on”. On the dark palette the two tokens already resolve to the same value, so nothing changed there.

Resist “making it clearer” with a heavier step such as --border or --secondary: a placeholder as dark as real text makes a loading page read like a broken loaded one. The pulse, not the weight, is what says “pending”. On a surface that is itself bg-muted the block nearly vanishes — put the panel on bg-card instead of recolouring the skeleton.

on card

on muted

Reduced motion

The base class list is bg-muted animate-pulse rounded-md, and the pulse is unconditional — the package does not wrap it in a prefers-reduced-motion guard. Add motion-reduce:animate-none where it matters, or switch it off once globally in your app stylesheet.

default
motion-reduce

Props

PropTypeDefaultDescription
classNamestringTailwind classes that define the placeholder’s size and shape — set width, height, and rounding here. Merged onto the base bg-muted animate-pulse rounded-md.
...restReact.ComponentProps<'div'>All native div attributes (style, id, aria-*, data-*) are forwarded to the underlying element, which also carries data-slot="skeleton".

Accessibility

  • Skeletons are purely decorative — wrap the loading region in a container with aria-busy="true" so assistive tech knows content is pending.
  • Announce completion with an aria-live region rather than relying on the visual swap, since screen reader users will not see the pulse animation.
  • The animate-pulse animation always runs — nothing in the package gates it on prefers-reduced-motion. Pass motion-reduce:animate-none, or disable it once in a global rule, so a screenful of pulsing blocks does not reach people who asked for less motion.
  • Match the skeleton’s footprint to the real content to avoid layout shift (CLS) when the data resolves.