Skip to content

Skeleton

Stable

A placeholder that previews the shape of content while it loads, so the page settles instead of jumping.

Installation

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/skeleton.json

Usage

import { Skeleton } from "@/components/ui/skeleton"
<Skeleton className="h-4 w-48" />

A skeleton is a shape and nothing more. Give it the size and radius of the content it stands in for — rounded-full for avatars, rounded-lg for buttons — and let the layout do the rest.

Examples

Card

Mirror the real component's structure. Reusing Card and its parts guarantees the placeholder occupies exactly the space the loaded card will.

List

Vary line widths. Identical bars read as a pattern, not as content, and make the swap to real text more jarring.

Table

Keep the real header and replace only the cells. People can start reading column names while data arrives.

InvoiceCustomerStatusAmount

Loading state

Swap skeletons for content in place. Mark the region with aria-busy while it loads and include a visually hidden message, so screen reader users know something is on its way.

Loading team members…
<div aria-busy={loading} aria-live="polite">
  {loading && <span className="sr-only">Loading team members…</span>}
  {loading ? <MembersSkeleton /> : <MembersList />}
</div>

Guidelines

When to use

  • For the first load of content whose layout you know: cards, lists, tables, profiles.
  • For content that loads in parts, so each section can resolve independently.

When not to use

  • For waits under about a second — the skeleton flashes and the page feels slower. Delay showing it, or show nothing.
  • For actions like saving or submitting — use the Button's loading state or a Spinner.
  • For measurable tasks — use Progress.
  • For content whose layout you can't predict. A wrong skeleton is worse than none.

Match the layout, not the pixels

A skeleton should occupy the same space as the content it replaces, so nothing moves when data arrives. It doesn't need every detail: an avatar, a line for the name, a shorter line for the meta is enough.

Do.Simple shapes in the real layout. Nothing shifts when content arrives.
Don’t.A generic block doesn't match the content and causes a jump on load.

Accessibility

  • Every skeleton renders aria-hidden="true": placeholders mean nothing to a screen reader. Describe the loading state on the container instead, with aria-busy="true" and a visually hidden message.
  • When the content arrives, remove aria-busy. With aria-live="polite" on the container, the new content is announced without moving focus.
  • The shimmer only animates translate on a pseudo-element, so it never triggers layout. Under prefers-reduced-motion the sweep is removed and the skeleton rests as a static tint.
  • Skeleton fills use the muted token and never carry information, so they're exempt from contrast requirements — but they stay visible in both themes and in high-contrast mode.

API reference

Skeleton

Renders a <div> with data-slot="skeleton" and aria-hidden="true". Accepts all div props.

PropTypeDefault
className

Size and shape. Set width, height and radius to match the content being replaced.

stringNo default
style

Useful for computed widths, e.g. varying line lengths in a list.

CSSPropertiesNo default