Skip to content
Monogem

Component · Navigation & Feedback

Skeleton

Overview

A placeholder shape shown in the spot where real content will appear once it loads — a greyed box the size of the avatar, the heading, the thumbnail. It reduces layout shift and signals “this is coming” without a spinner. Use it for content whose shape is predictable before the data arrives.

Anatomy

A single block. No sub-parts — like Separator, Skeleton has nothing to compose. A skeleton screen is several Skeletons laid out by the page to mirror the real content’s structure; that layout is the composer’s, not a Skeleton variant.

Live Example

Text lines

Avatar + lines

Card block

Code Example

skeleton-showcase.tsx
import { Skeleton } from "@/components/ui/skeleton";

/**
* Live Skeleton example — Skeleton is a server component (no client JS). Shape
* comes entirely from `className`. Shown: stacked text lines, an avatar circle
* + two lines, and a card-shaped block. The wrapper that would swap in real
* content owns `aria-busy` / "Loading…" — the skeletons themselves are
* `aria-hidden`.
*/
export function SkeletonShowcase() {
return (
<div className="flex flex-col gap-8">
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Text lines
</h3>
<div className="flex max-w-sm flex-col gap-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/5" />
</div>
</section>

<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Avatar + lines
</h3>
<div className="flex items-center gap-3">
<Skeleton className="size-10 rounded-full" />
<div className="flex flex-col gap-2">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-24" />
</div>
</div>
</section>

<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Card block
</h3>
<div className="flex max-w-sm flex-col gap-3">
<Skeleton className="aspect-[16/9] w-full" />
<Skeleton className="h-5 w-2/3" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-4/5" />
</div>
</section>
</div>
);
}

Variants

None. Size and shape come entirely from className:

Building thisRecipe
Text lineh-4 w-full (last line w-3/5 to look like ragged text)
Avatarsize-10 rounded-full
Thumbnail / mediaaspect-[16/9] w-full
Full-bleed blockrounded-none

--radius-md is the default corner; override with rounded-full / rounded-none / any radius utility.

States

StateLook
Default--muted fill, gentle pulse (animate-pulse)
Reduced motionPulse off (motion-reduce:animate-none) — a static --muted block

Skeleton has no interactive states — it is a decorative placeholder, replaced by real content when loading finishes.

Usage Guidance

Tokens

Skeleton uses existing Monogem semantic and primitive tokens to stay consistent with the design system; it introduces no component-specific styling values.

TokenWhere used
--mutedBlock fill — Color names this token for “subtle backgrounds”
--radius-mdDefault corner radius

The pulse is Tailwind’s animate-pulse (an opacity oscillation), the same loading-motion idiom the rest of the system uses; no keyframe of Monogem’s own.

Do / Don’t

Do

  • Match the skeleton’s size and shape to the real content it stands in for, so nothing jumps when the content arrives.
  • Group skeletons into a layout that mirrors the real structure (avatar + two lines, card + title + body).
  • Put aria-busy="true" on the region that is loading, and give it a visually hidden “Loading…” (or a polite live region) — see Accessibility.
  • Remove skeletons as soon as the real content is ready; don’t cross-fade for long.

Don’t

  • Use a Skeleton when you don’t know the content’s shape — a plain loading indicator is honester.
  • Leave skeletons on screen as a permanent empty state — that’s an empty-state message, not a loading placeholder.
  • Announce each skeleton to assistive tech — they’re decorative and would be noise.
  • Reach for Skeleton to show progress — that’s Progress.

Accessibility

  • Each Skeleton is aria-hidden="true" — the fake shapes carry no information a screen reader user needs, and announcing a dozen of them would be noise. The attribute is applied after {...props}, so a passed-in aria-hidden={false} can’t silently turn the decorative block into an announced one.
  • The loading state belongs to the surrounding experience, not the placeholder: the region that will receive the content sets aria-busy="true" while it loads and exposes a visually hidden “Loading…” or a polite role="status" live region, then removes both when the content is in. Skeleton itself says nothing.
  • Motion: the pulse respects prefers-reduced-motion via motion-reduce:animate-none. A reduced-motion user sees a static --muted block, which still communicates “placeholder.”

Related Components / Patterns

  • Progress — how far along a task is.
  • Table — shows Skeleton rows while loading.