Skeleton
Loading placeholder that mimics the shape of real content while data is on the way. A single primitive — <Skeleton /> — that renders as an animated pulse block (bg-muted, rounded-md by default). Everything else is composition: size it with height / width classes, round the corners to match the target (rounded-full for avatars), and stack multiple blocks to preview a whole row, card or page.
Anatomy
One primitive, three canonical shapes assembled from it. A block is just a Skeleton with size classes; a text line is a Skeleton with h-4 (16px, matching text-sm line-height); an avatar is a Skeleton with size-10 + rounded-full.
- 1Skeleton (block)
The primitive: <div className='animate-pulse rounded-md bg-muted' />. Sized entirely by the className you pass — h-24 w-full for a hero block, size-10 rounded-full for an avatar circle, h-4 w-1/3 for a text line.
- 2Text line
Skeleton with h-4 (16px, matches text-sm leading-normal) and a variable width to hint at the copy length. Two or three stacked lines with gap-2 read as a paragraph.
- 3Avatar
Skeleton with size-10 rounded-full for the standard 40px user thumbnail. Swap to size-5 / size-6 for compact rows, size-14 for account cards.
- 4Card / thumb
Skeleton with h-40 (or aspect-video, aspect-square) for image or media placeholders. Keep rounded-md by default; use rounded-lg to match Card.
Variants
One primitive, three canonical assemblies straight from Figma. Everything else is a mix-and-match of these blocks.
<div className="flex items-center gap-4">
<Skeleton className="size-10 rounded-full" />
<div className="flex flex-col gap-2">
<Skeleton className="h-4 w-[150px]" />
<Skeleton className="h-4 w-[100px]" />
</div>
</div><div className="flex w-[334px] flex-col gap-6">
<div className="flex flex-col gap-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
</div>
<Skeleton className="aspect-square w-full" />
</div><div className="flex flex-col gap-2">
<Skeleton className="h-4 w-[222px]" />
<Skeleton className="h-4 w-[167px]" />
</div>Motion
The pulse uses Tailwind's animate-pulse: opacity oscillates 100% → 50% → 100% every 2s (cubic-bezier(0.4, 0, 0.6, 1) infinite). It's a low-key pulse — the pattern reads as 'something is coming' without competing with content that's already on screen. Respect prefers-reduced-motion at the app level (a global rule can freeze animate-pulse to a static bg-muted block).
Recipes
Ready-to-copy compositions covering the most common Yuno usages of this atom.
Circle avatar + two stacked text lines (title + subtitle). The canonical placeholder for account lists, comment threads, activity feeds, and member pickers.
<div className="flex items-center gap-4">
<Skeleton className="size-10 rounded-full" />
<div className="flex flex-col gap-2">
<Skeleton className="h-4 w-[150px]" />
<Skeleton className="h-4 w-[100px]" />
</div>
</div>Two title/subtitle lines above a square media block. Use for photo galleries, upload previews or any card that leads with an image.
<div className="flex w-[334px] flex-col gap-6">
<div className="flex flex-col gap-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
</div>
<Skeleton className="aspect-square w-full" />
</div>Two stacked h-4 lines with slightly different widths (222px + 167px in Figma) hint at a paragraph in flow. Use for descriptions, article summaries or copy blocks.
<div className="flex flex-col gap-2">
<Skeleton className="h-4 w-[222px]" />
<Skeleton className="h-4 w-[167px]" />
</div>A row of Skeletons with widths that echo the real column widths. Repeat 3-5 rows inside the tbody while the data loads.
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="flex items-center gap-4 border-b px-4 py-3 last:border-b-0">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-40" />
<Skeleton className="h-4 w-16" />
<Skeleton className="ml-auto h-4 w-20" />
</div>
))}One small label line (h-3 w-1/3) above a large value block (h-8 w-2/3). Preview a dashboard tile without the whole numbers popping in later.
<div className="flex flex-col gap-3 rounded-lg border p-5">
<Skeleton className="h-3 w-1/3" />
<Skeleton className="h-8 w-2/3" />
</div>Import
One import, one export.
import { Skeleton } from "@/components/ui/skeleton";Props
Skeleton forwards all HTMLDivElement attributes. The only prop that matters day-to-day is className.
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | — | Merged onto the div. Where all the shape lives — sizes (h-4, w-1/3, size-10), radii (rounded-full for avatars, rounded-md default, rounded-lg for cards), and aspect ratios (aspect-square, aspect-video). Do not override bg-muted. |
| ...HTMLAttributes | React.HTMLAttributes<HTMLDivElement> | — | All standard div attributes forward (id, role='status', aria-label, data-*, etc). Add role='status' + aria-label='Loading' when the Skeleton is the only content in a live region. |
When to use
- Card lists, table rows, avatar + label combos while the data loads.
- Any structural loading state where a spinner would feel too abrupt for the surface size.
- Any surface where the layout can be reserved before the data arrives (KPI tiles, detail sheets, chart canvases).
When not to use
- As decoration — Skeleton signals loading, not visual filler.
- For very short waits (< 100ms) — the flicker distracts more than it reassures.
- For fully async surfaces you can't measure the shape of — use an empty state or a spinner instead.
Usage
- Match the Skeleton size to the real content dimensions (h-4 for text-sm, size-10 for the default avatar, h-9 for a Button).
- Round the block to match the target: rounded-full for circles, rounded-md for text/blocks, rounded-lg for cards.
- Remove the Skeleton as soon as the data lands — no fade-out gimmicks.
- Vary widths across stacked text lines so the block reads as a paragraph, not a single bar.
- Don't leave it visible for more than ~3 seconds — swap to an empty state or an error hint.
- Don't tint the pulse (bg-primary, semantic colors) — bg-muted is intentional so the pulse recedes.
- Don't animate multiple pulses with different rhythms — Tailwind's animate-pulse is the only cadence.
- Don't skeleton a whole page when only one region is loading — reserve the interactive chrome.
Related
Cross-links to atoms and patterns you may reach for next.
- ProgressReach for Progress when you can actually measure the wait — Skeleton is for unmeasured 'coming soon'.
- CardSkeleton often lives inside a Card while its content loads. Keep the outer Card visible; only skeleton the content.
- TableTable + Skeleton is the Dashboard's most common loading state — skeleton the tbody rows, keep the thead labels.
- AvatarSame size-10 rounded-full shape — a Skeleton avatar swaps directly for a loaded one.