A placeholder shape where content will be, so a loading page keeps its layout.

Shapes

A line is the height of one line of the text it stands for and fills its holder's width; a block is a field tall; a circle is 40px, the size of a target. Set width and height as CSS lengths, such as 12rem or 60%.

Example

Follows the page theme and corners
import { Skeleton } from '@snapshot-ui/react';
import type { ReactElement } from 'react';

export default function SkeletonShapes(): ReactElement {
  return (
    <div
      style={{ display: 'grid', gap: 'var(--spacing-part)', width: 'min(28rem, 100%)', marginInline: 'auto' }}
    >
      <Skeleton />
      <Skeleton width="12rem" />
      <Skeleton shape="block" />
      <Skeleton shape="circle" />
    </div>
  );
}

A card that is loading

Draw the shape of what is coming: a circle and two lines for a header, a block for the body. When the content arrives it takes the same room, so nothing on the page moves.

Example

Loading

Follows the page theme and corners
import { Card, CardBody, Skeleton } from '@snapshot-ui/react';
import type { ReactElement } from 'react';

export default function SkeletonCard(): ReactElement {
  return (
    <div style={{ maxWidth: 480, marginInline: 'auto' }}>
      {/* The status sits outside the busy part; visually-hidden is this site's class, so use your own. */}
      <p role="status" className="visually-hidden">
        Loading
      </p>
      <div aria-busy="true">
        <Card>
          <CardBody>
            <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-part)' }}>
              <Skeleton shape="circle" />
              <div style={{ flex: 1, display: 'grid', gap: '0.25rem' }}>
                <Skeleton width="60%" />
                <Skeleton width="40%" />
              </div>
            </div>
            <Skeleton shape="block" height="6rem" />
            <Skeleton width="30%" />
          </CardBody>
        </Card>
      </div>
    </div>
  );
}

Usage

Skeleton is static. It renders in a server component and ships no client JavaScript. It is hidden from assistive technology, so say "Loading" once in your own status, as the data table does, and mark the holder aria-busy. Keep the status outside the busy holder and mounted all the time, and give it its text when loading starts: a screen reader may hold back what changes inside a busy part, and a live region that appears with its text already in it is often not announced. Size each shape as the content it stands for. The shape pulses gently, and stands still when the reader asks for reduced motion.

import { Skeleton } from '@snapshot-ui/react';

export function LoadingRow({ isLoading }: { isLoading: boolean }) {
  return (
    <>
      {/* Your app's visually hidden class: the library ships none. */}
      <p role="status" className="visually-hidden">
        {isLoading ? 'Loading' : ''}
      </p>
      <div aria-busy={isLoading}>
        <Skeleton shape="circle" />
        <Skeleton width="60%" />
      </div>
    </>
  );
}

Props

className is merged after the skeleton's own class.

className
string

height
string

A CSS length; a line is one line of the text it stands for. A circle without width takes it as its diameter.

shape
"block" | "circle" | "line"

Default: 'line'

width
string

A CSS length; a line defaults to 100% and a circle to 40px. A circle takes it as its diameter.

Accessibility

A skeleton is aria-hidden, holds no text and takes no focus. Saying that the page is loading is the holder's job: one polite status, outside the busy part and always mounted, that says "Loading", and aria-busy on the part that is loading. In forced colors the shape keeps a fill of its own, so it does not vanish.

Design rules

What keeps skeletons in the system.

Draw the shape of what is coming, at its size.

Say Loading once, in one status line.

Never show a skeleton for a fetch shorter than a moment.

Never put text inside a skeleton.

Never put a spinner beside skeletons. One loading signal is enough.

esc

↑ ↓ to move↵ to open