Components

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.

Updated Jul 17, 2026 by Leonardo Posada

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.

  1. 1
    Skeleton (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.

  2. 2
    Text 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.

  3. 3
    Avatar

    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.

  4. 4
    Card / 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.

Default (user row)
<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>
Card
<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>
Text lines
<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).

Match the shape, not the color
The Skeleton's job is to reserve the exact layout the loaded content will occupy so nothing shifts on arrival. Match heights (h-4 for a text-sm line, h-9 for a Button), widths (percentage or fraction, not fixed pixels), radii (rounded-full for circles, rounded-md for blocks) — but leave the color as bg-muted. Don't tint the pulse to hint at brand or state; the pulse only signals 'loading'.

Recipes

Ready-to-copy compositions covering the most common Yuno usages of this atom.

User row (Figma default)

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>
Card

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>
Text paragraph

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>
Table row

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>
))}
KPI card

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.

One primitive. Everything else is className composition.
import { Skeleton } from "@/components/ui/skeleton";

Props

Skeleton forwards all HTMLDivElement attributes. The only prop that matters day-to-day is className.

PropTypeDefaultDescription
classNamestringMerged 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.
...HTMLAttributesReact.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

Do
  • 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
  • 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.