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

PropTypeDefaultWhat it does
classNamestring—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.