Skeleton
StableA 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.jsonUsage
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.
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.
<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
loadingstate 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.
Accessibility
- Every skeleton renders
aria-hidden="true": placeholders mean nothing to a screen reader. Describe the loading state on the container instead, witharia-busy="true"and a visually hidden message. - When the content arrives, remove
aria-busy. Witharia-live="polite"on the container, the new content is announced without moving focus. - The shimmer only animates
translateon a pseudo-element, so it never triggers layout. Underprefers-reduced-motionthe sweep is removed and the skeleton rests as a static tint. - Skeleton fills use the
mutedtoken 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.