Skeleton
A skeleton is a grey placeholder that holds the shape of content while it loads.
import { Skeleton } from "@/components/ui/skeleton";
export default function SkeletonDemo() {
return (
<>
<div className="flex w-full max-w-sm items-center gap-4" aria-busy="true">
<Skeleton className="size-12 shrink-0 rounded-full" />
<div className="flex flex-1 flex-col gap-2">
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-1/2" />
</div>
</div>
<div className="flex w-full max-w-sm flex-col gap-3" aria-busy="true">
<Skeleton className="h-32 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-2/3" />
</div>
</>
);
}The code
The component is a single file, read from the repository when the site is built. Press Show code to see all of it.
import type { ComponentProps } from "react";
import { cx } from "@/lib/cx";
// A placeholder that holds the shape of content that is still loading. It has no size of its
// own: give it one with classes (h-4 w-40 for a line, size-12 rounded-full for an avatar).
// It pulses, except for people who ask their system for reduced motion, and it is hidden from
// assistive tech, so mark the region that is loading with aria-busy instead.
export function Skeleton({ className, ...props }: ComponentProps<"div">) {
return (
<div
aria-hidden
className={cx("rounded-md bg-border-strong motion-safe:animate-pulse", className)}
{...props}
/>
);
}Props
| Prop | Type | Default | What it does |
|---|---|---|---|
className | string | — | Gives it a size and shape: h-4 w-40 for a line, size-12 rounded-full for an avatar. |
Skeleton is a plain div and takes every normal prop. It has no size of its own.
The thinking
A skeleton is a promise that content is coming, in a particular shape. Keep the promise by matching the shape.
- Same shape as the content: Size each skeleton like the thing it replaces, so the page doesn't jump when the real content arrives.
- No variants: Shape and size come from classes, so one component covers a line, a block and a circle.
- Only when you know: Use a skeleton when you know what is coming. If you don't know how long it will take, use a Spinner, or a Progress bar when you can measure it.
Interface design
- Fill: The strong border color, with the md radius, 6px, unless you change it.
- Pulse: It fades to half its opacity and back, once every two seconds.
Interaction
- Not interactive: A skeleton has no hover, focus or pressed state.
- Reduced motion: For people who ask their system for less motion, it stops pulsing and stays as a still grey shape.
Empathy
- Hidden from screen readers: A grey shape means nothing when spoken, so each skeleton is hidden from assistive tech. Put
aria-busy="true"on the region that is loading, as the demo does. - Say it is loading: Pair a skeleton with a visually hidden status such as Loading, so people who can't see it still know.
- Don't leave it forever: Replace it with the content, or with an error message if it fails. A skeleton that never resolves looks like a broken page.