Skeleton

Holds the space of content that is still loading.

Examples

A run row

A list of runs

Card text

Text style

  • large heading

  • small heading

  • bodydefault

  • small body

  • label

  • code

Box

  • avatar

  • button

  • image

Usage

When to use it
Use
Content with a known shape is loading: rows, a header, a chart frame.
Use
The load usually takes 150ms to a few seconds.
Not for
The shape is unknown, or the wait is a task with an amount. Use Progress.
Not for
A button waits on its own action. Use Button.
Not for
The load failed. Use Empty state.

Rules

8 rules
Must
Wrap loading content in SkeletonSwap, not a loading ternary; DataTable, Chart and Stat take their own loading prop instead. Because a bare ternary flashes the skeleton on a 40ms response and blanks the content on every refetch.
Must
Use SkeletonText with the same type role and line count as the real text. Because each line is 1lh of its role, so a wrong role or a fixed h-4 box makes the rows jump when the text arrives.
Should
Set SkeletonSwap label to what is loading: 'Loading runs'.
Must
Keep skeletons aria-hidden and let SkeletonSwap announce once. Because a role or label on each block makes a screen reader read "loading" once per row.
Never
Add a pulse or sweeping gradient to a Skeleton; the block is still. Because a sweep across a dozen rows pulls the eye from the content that has loaded and ignores reduced motion.
Should
Render only as many skeleton rows as fit the region, at most 8.
Should
Size a Skeleton box with the classes of the real thing: size-8 for an avatar, h-(--control) for a button.
Must
Time out a load that never ends and show an error Empty state in the same box. Because SkeletonSwap holds the skeleton while loading stays true, so a hung request reads as loading forever.

Accessibility

4 checks
Skeleton and SkeletonText are aria-hidden and take no focus.
The swap region sets aria-busy while held or stale.
A role="status" region announces the label once, when the skeleton shows; a reload announces nothing.
Forced colors paint the fill GrayText so the shape stays visible.

Edge cases

10 edge cases
One-line stub at w-8
Stays a visible 32px stub.
lines={0}
Clamps to one line so no zero-height block shifts later.
Container 120px
Text lines shrink to the container; fixed boxes keep their size.
Right to left
Lines start at the inline start; the short last line ends early on the left.
Zoom 200%
Line heights follow the role and still match the text.
Response inside 150ms
Swaps straight to content; the skeleton never shows.
Loaded within 400ms of showing
The skeleton stays to its minimum so it does not blink.
Reload after content shown
Content stays, aria-busy at once, 60% opacity after 150ms; no skeleton.
Load never ends
Skeleton and aria-busy stay forever; the caller times out.
Inside a well or table
The fill is the grey field, so it nearly vanishes on grey; keep skeletons on paper.

API

9 props
Skeleton
div props
SkeletonText type
TextRole default 'body'
SkeletonText lines
number default 1
SkeletonText last
number default 0.6
SkeletonSwap loading
boolean default required
SkeletonSwap fallback
ReactNode default required
SkeletonSwap delay
number default 150
SkeletonSwap minDuration
number default 400
SkeletonSwap label
string default 'Loading'

Tokens

6 tokens
--surface-skeleton
color-mix(in oklch, oklch(var(--grey-c)), oklch(var(--ink-c)) var(--skeleton-step))
--radius-soft
6px
--radius-inner
4px
--dur-skeleton-delay
150ms
--dur-skeleton-hold
400ms
--stale-opacity
0.6

Install

For your coding agent

GitHub

Please star me pookie

141

A new design system every week

Leave your email before you copy the prompt.