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
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 this | Recipe |
|---|---|
| Text line | h-4 w-full (last line w-3/5 to look like ragged text) |
| Avatar | size-10 rounded-full |
| Thumbnail / media | aspect-[16/9] w-full |
| Full-bleed block | rounded-none |
--radius-md is the default corner; override with rounded-full / rounded-none / any radius
utility.
States
| State | Look |
|---|---|
| Default | --muted fill, gentle pulse (animate-pulse) |
| Reduced motion | Pulse 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.
| Token | Where used |
|---|---|
--muted | Block fill — Color names this token for “subtle backgrounds” |
--radius-md | Default 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-inaria-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 politerole="status"live region, then removes both when the content is in. Skeleton itself says nothing. - Motion: the pulse respects
prefers-reduced-motionviamotion-reduce:animate-none. A reduced-motion user sees a static--mutedblock, which still communicates “placeholder.”