# Skeleton

A gray block that holds the place of content while it loads, in the shape and size of what will replace it: a line of text, an avatar, a control, an image. It pulses unless the user asked for reduced motion.

## Usage

### When to use

- Hold the place of content whose layout you know while it loads: a list of comments, a card, a profile.
- Keep the page from jumping when the content comes in.

### When not to use

- An action that takes a moment, such as saving. Use Spinner.
- Work whose amount is known, such as an upload. Use Progress.

## Variants

### shape

text stands in for one line of the text around it; circular for an avatar; control for a Button, Input or Select; rectangular for an image or a block of media.

Values: `text`, `circular`, `rectangular`, `control`. Default: `text`.

### size

circular and control only. circular takes the Avatar sizes (24, 32, 40px; Avatar has no xl, so xl is the 40px lg circle); control the element heights, so it matches a control of the same size.

Values: `sm`, `md`, `lg`, `xl`. Default: `md`.

## Examples

### A comment loading

An avatar and two lines of text, in the layout of the comment that replaces them.

```tsx
<SkeletonGroup className="flex w-full max-w-sm items-start gap-md">
  <Skeleton shape="circular" />
  <div className="flex flex-1 flex-col text-sm">
    <Skeleton className="w-32" />
    <Skeleton lines={2} />
  </div>
</SkeletonGroup>
```

### A card loading

```tsx
<SkeletonGroup className="flex w-64 flex-col gap-md">
  <Skeleton shape="rectangular" />
  <Skeleton className="text-md w-40" />
</SkeletonGroup>
```

### A form loading

A label line at the label's size and a control at the field's size, so the form does not move when it arrives.

```tsx
<SkeletonGroup label="Loading settings" className="flex w-64 flex-col gap-sm">
  <Skeleton className="w-24 text-sm" />
  <Skeleton shape="control" />
</SkeletonGroup>
```

## Do and don't

### Do

- Match the layout of the content that replaces it, so nothing jumps when it loads: text lines in the same font size, controls at the same size, the same corners.
- Use lines for a paragraph: the last line is shorter, like real text.
- Wrap the region in SkeletonGroup, or set aria-busy and a polite "Loading" status yourself.
- Show it only when loading takes longer than about 150ms, and keep it at least about 300ms once shown (useSkeletonDelay), so it never flashes.
- For a tab, a page of a table or a filter, keep the previous content on screen with a pending state instead of a skeleton.

### Don't

- Don't use it for an action that takes a moment, such as saving. Use Spinner.
- Don't use it when the amount of work is known, such as an upload. Use Progress.
- Don't put text, icons or controls inside it.
- Don't blank content that is already on screen to show a skeleton.

## Props

| Name | Type | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `shape` | `enum` | `text`, `circular`, `rectangular`, `control` | `text` | text stands in for one line of the text around it; circular for an avatar; control for a Button, Input or Select; rectangular for an image or a block of media. |
| `size` | `enum` | `sm`, `md`, `lg`, `xl` | `md` | circular and control only. circular takes the Avatar sizes (24, 32, 40px; Avatar has no xl, so xl is the 40px lg circle); control the element heights, so it matches a control of the same size. |
| `width` | `string` |  |  | Width for a value known only at runtime, such as a column width. Otherwise set a size class with className (w-48). |
| `height` | `string` |  |  | Height for a value known only at runtime. Otherwise set a size class with className (h-24). With lines, the height of each line. |
| `lines` | `number` |  | `1` | Text only: that many lines in a column, the last one shorter, like the end of a paragraph. className and width go on the column. |
| `label` | `string` |  | `Loading` | SkeletonGroup only: what screen readers hear while the region loads. |

## Anatomy

| Part | Description |
| --- | --- |
| `root` | The block in the soft gray fill. text is a box one line tall (the line height of its font size) with a bar one em tall in the middle, inner corners; circular is an Avatar-sized circle; control is a control-sized block (element height, element corners); rectangular is full width at 16 by 9 with element corners. |
| `bar` | text only: the bar inside the line box, drawn as a ::before. |
| `lines` | With lines: the column that holds the lines, with no gap, so it is as tall as the same number of real lines. The last line is two thirds wide. |
| `group` | SkeletonGroup: the region that loads, with aria-busy, and a visually hidden polite status next to it that reads label. |

## Accessibility

Role: `none`

- Hidden from screen readers (aria-hidden). The region that loads sets aria-busy="true" and says "Loading" in a polite status; SkeletonGroup does both. Remove both when the content is in.
- The pulse is an opacity loop timed by duration-loop, and it does not run at all under prefers-reduced-motion; the block stays.
- Never focusable. Controls in the loading region stay out of the tab order until they exist.

## Install

```bash
npx shadcn add https://werewolf-ui.vercel.app/r/skeleton.json
```

This also adds `styles/werewolf.css`, the tokens every component uses, and any other component it needs. Import that file once in your root CSS, after `@import "tailwindcss"`, with a path relative to that CSS file (from `app/globals.css` it is `@import "../styles/werewolf.css"`).
