Skip to content

Skeleton

Hero example

When to use

Reach for Skeleton when a piece of content has a known shape and a measurable load time. A skeleton matches the eventual layout, so the surrounding chrome does not shift on resolve, and the user sees that something is loading without reading a label.

Do not use Skeleton for content that resolves under one frame. Do not use it for content that may never load (use an empty state instead). For full-page loading inside Next.js App Router, compose Skeletons under PageSkeleton and drop the result into a loading.tsx Suspense boundary.

Do — size the Skeleton to the eventual content. Heights of 12 to 14 pixels match body lines; 22 pixels matches pill heights; 28 pixels matches H3 ranges; 36 pixels matches inputs.
Don't — render a wall of identical-width Skeletons. Vary widths slightly across stacks so the placeholder reads as prose, not bars.

Anatomy

A single <div aria-hidden> with the .m-skeleton recipe. The shimmer sweep lives on an ::after pseudo-element so the track and the highlight can transition independently. Width and height pass through as inline style; the radius and static props surface as data-* attributes so CSS can switch shapes without re-rendering.

The track color reads from --stone-200. The highlight sweep reads from --stone-300 and travels left to right every 3 seconds with linear motion, matching the canonical Matter shimmer cadence used by Pill shimmer and the marquee text reveals.

Props

PropTypeDefaultDescription
widthnumber | string
heightnumber | string
radiusSkeletonRadius"md"
staticbooleanRenders the track without the shimmer sweep. Use when many skeletons stack on one page — keep one or two animated, the rest static, so the surface doesn't strobe.
classNamestring
styleReact.CSSProperties

Also accepts Omit<React.HTMLAttributes<HTMLDivElement>, "children" | "style">.

Composition

Two thin composers ship alongside the primitive:

  • SkeletonText renders N stacked single-line Skeletons. The last line narrows to 60 percent by default so a paragraph placeholder reads as prose rather than bars.
  • SkeletonAvatar renders a circular Skeleton sized to match the Avatar atom — 24, 32, or 48 pixels.
import { Skeleton, SkeletonAvatar, SkeletonText } from "@matter/components";

export function PersonRowSkeleton() {
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
      <SkeletonAvatar size="md" />
      <div style={{ flex: 1, display: "flex", flexDirection: "column", gap: 6 }}>
        <Skeleton height={14} width={180} />
        <Skeleton height={10} width={120} />
      </div>
      <Skeleton height={22} radius="pill" width={72} />
    </div>
  );
}

States

Skeleton has two states, controlled by the static prop:

  • default — the shimmer sweep animates continuously
  • static — only the track renders; useful when a single page stacks many Skeletons and a wall of animated highlights would strobe

Themes

Tokens consumed

  • --stone-200 for the track background
  • --stone-300 for the highlight sweep
  • --radius-sm, --radius-md, --radius-lg for the four radius variants (pill resolves to 999px)

Accessibility

  • Keyboard interactions. None. Skeleton is presentational; focus belongs on the resolved content once it mounts.
  • ARIA roles and properties. Each Skeleton renders aria-hidden. Wrap stacks in a role="status" region with aria-busy="true" and aria-label="Loading…" so assistive tech announces the load once per region, not once per primitive. PageSkeleton does this for you.
  • Reduced motion. Under prefers-reduced-motion: reduce, the shimmer sweep stops; the track stays visible so the placeholder shape still communicates loading.
  • Forced colors. Under forced-colors: active, the track switches to GrayText and the gradient sweep is hidden.

Do / Don't

Do — match Skeleton sizes to the eventual content height so the layout does not jump on resolve.
Don't — animate every Skeleton on the page. Pick a few load-bearing shapes and pass static on the rest.
Do — wrap related Skeletons in a single role="status" region (or use PageSkeleton) so screen readers announce one load, not many.
Don't — pair a Skeleton with a label like "Loading" written in caps. The canonical phrase is Loading… in sentence case.

Recipes

  • A list-row placeholder uses one 14-pixel-tall Skeleton for the title, one 10-pixel-tall Skeleton for the meta, and a 22-pixel pill-radius Skeleton for a trailing chip.
  • A card placeholder stacks a 14-pixel header skeleton, an 18-pixel title, and four rows of body skeletons.
  • A grid of stat tiles uses 120-pixel-tall Skeletons with radius="lg".

Code example

import { Skeleton } from "@matter/components";

export function StatTile() {
  return <Skeleton height={120} radius="lg" />;
}

Source

packages/components/src/Skeleton

On this page