Skeleton
Holds the space of content that is still loading.
Examples
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
Skeletondiv propsSkeletonText typeTextRoledefault'body'SkeletonText linesnumberdefault1SkeletonText lastnumberdefault0.6SkeletonSwap loadingbooleandefaultrequiredSkeletonSwap fallbackReactNodedefaultrequiredSkeletonSwap delaynumberdefault150SkeletonSwap minDurationnumberdefault400SkeletonSwap labelstringdefault'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