Components · Data display & feedback
Skeleton
A loading placeholder that mirrors the shape of the content it stands in for, with a subtle shimmer sweep. Use it instead of a spinner when the layout is known in advance.
Preview
Build with the skill
No package to install — hand this to your AI to generate or migrate the component in your stack.
Apply the Zetta "Skeleton" component to this project using the zetta-design-md skill.
WHEN TO USE
- Migrating or fixing existing code (the common case): audit the current implementation against the `skeleton` spec and CORRECT every deviation — replace off-spec values, don't only add new ones.
- New component: build it to the `skeleton` spec in this project's stack.
AUDIT & FIX (when the component already exists)
Compare it to the skill's spec and fix, don't just append: hardcoded colors/hex, off-scale radius, wrong fonts, dark/navy fills, shadows used in place of borders (or missing borders), brand lime used as a CTA or interaction (must be primary indigo), and any missing or wrong hover / focus / active / disabled / invalid state or missing 2px focus ring. Replace each with the correct Zetta token.
ALWAYS
- Pull the authoritative spec and tokens from the skill — never guess or hardcode values.
- Use this project's own component conventions and framework idioms.
- Honor the Zetta guardrails (brand = identity only, primary = interaction, shadows for overlays) and verify in Light, Dark, and Accessibility. Anatomy & rules
╭─ skeleton-row ──────────────────────────────────────────╮
│ ● ████████████████ ← circle-md + text-line │
│ ████████ ← text-line (short) │
╰─────────────────────────────────────────────────────────╯
╭─ skeleton-rectangle-md ─────────────────────────────────╮
│ [████████████████████████] ← card image placeholder │
╰─────────────────────────────────────────────────────────╯
████████████████ ← skeleton-text-title
████████████████████████████ ← skeleton-text
████████████████████████ ← skeleton-text-short
| State | Visual |
|---|---|
| Loading | {colors.skeleton-bg} fill with shimmer animation |
| Shimmer | Linear gradient sweeping left to right, {motion.duration-slow} loop |
When to use
- Content that takes > 300ms to load.
- Replace entire sections — not individual words within a sentence.
When not to use
- Operations under 300ms — an immediate result is better than a flash of skeleton.
- Error states — use Section Message or Empty State instead.
- Spinners are better when the full layout is unknown (e.g. dynamic search results with unpredictable shape).
Do
- Match skeleton shapes to the actual content as closely as possible — same number of lines, same approximate widths.
- Use
skeleton-rowandskeleton-blockto compose realistic layout placeholders. - Always remove skeletons and replace with real content — never leave them visible after load completes.
Don't
- Don't animate skeletons if
prefers-reduced-motionis active. - Don't use skeleton for interactive elements — buttons and inputs should be hidden or disabled during loading, not replaced with skeletons.
- Match skeleton shape to the real content: text lines for text, circles for avatars, rectangles for images.
- Vary skeleton line widths to look natural — 60%, 80%, 90% of container width for text lines.
- Never show skeleton and real content simultaneously.
- Accessible label: the skeleton container should have
aria-busy="true"and a visually hidden "Loading…" label.
Keyboard
Skeleton is a visual placeholder and has no keyboard behavior. It is replaced by real content once loading completes.
Accessibility
- All skeleton elements:
aria-hidden="true"— they are decorative placeholders. - The loading region should have
role="status"with a visually hidden live announcement: "Loading content…". - When loading completes, update the live region: "Content loaded" or remove it.
- Never make skeletons focusable —
tabIndex="-1"or omit from tab order entirely.
Spec
Values and token references resolved straight from the Zetta spec.
skeleton
- backgroundColor
- {colors.skeleton-bg}
- shimmerColor
- {colors.skeleton-shimmer}
- borderRadius
- {rounded.base}
- animationDuration
- 1.5s
- animationTimingFunction
- ease-in-out
- animationIterationCount
- infinite
skeleton-text
- height
- 14px
- borderRadius
- {rounded.full}
- backgroundColor
- {colors.skeleton-bg}
- width
- 100%
skeleton-text-short
- width
- 60%
skeleton-text-title
- height
- 20px
- borderRadius
- {rounded.full}
- backgroundColor
- {colors.skeleton-bg}
- width
- 40%
skeleton-circle
- borderRadius
- {rounded.full}
- backgroundColor
- {colors.skeleton-bg}
skeleton-circle-sm
- width
- 24px
- height
- 24px
skeleton-circle-md
- width
- 40px
- height
- 40px
skeleton-circle-lg
- width
- 48px
- height
- 48px
skeleton-rectangle
- borderRadius
- {rounded.base}
- backgroundColor
- {colors.skeleton-bg}
- width
- 100%
skeleton-rectangle-sm
- height
- 80px
skeleton-rectangle-md
- height
- 160px
skeleton-rectangle-lg
- height
- 240px
skeleton-row
- display
- flex
- alignItems
- center
- gap
- {spacing.sm}
skeleton-block
- display
- flex
- flexDirection
- column
- gap
- {spacing.sm}