# Skeleton

Loading placeholders drawn in dither, for blocks, text lines and avatars, with a dense band that sweeps across or a pulse that steps through the Bayer levels.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. The dithercn agent skill, for building any dither on the engine, is at [/.well-known/agent-skills/dithercn/SKILL.md](/.well-known/agent-skills/dithercn/SKILL.md).

> **This is an example, not the only option.** It is one component built on the dithercn engine. Install it when it is what you were asked for. For any other look, don't stretch its props to fit: write your own field on `DitherShader` ([Write your own dither](/docs/engine/write-your-own.md)), and read this example's source for technique. Its full source is in [https://dithercn.dev/r/skeleton.json](https://dithercn.dev/r/skeleton.json), under `files[].content`.

## Installation [#installation]

```bash
npx shadcn@latest add https://dithercn.dev/r/skeleton.json
```

## Usage [#usage]

```tsx
import { Skeleton } from "@/components/dithercn/skeleton";

export function ProfileLoading() {
  return (
    <div aria-busy="true" className="flex items-center gap-3">
      <span className="sr-only">Loading profile</span>
      <Skeleton shape="circle" size={40} />
      <Skeleton shape="text" lines={2} />
    </div>
  );
}
```

A skeleton is a block by default: give it a size with `className`. `shape="text"` draws a stack of lines that fills its width, the last one shorter. `shape="circle"` draws an avatar of `size` pixels.

Each skeleton is drawn in one colour. At rest it is a faint tint under a Bayer ramp that thins out from the left edge, so even a still frame reads as dither. `sweep` runs a wide band across it in four slices, each a step up the Bayer ramp, so the middle of the band is densest and its sides fall off level by level. `pulse` steps the whole block up through three nested Bayer levels and back, so the dots multiply and thin out rather than fade. The default colour is grey; use your ink or a palette colour where the real content will be coloured.

## Examples [#examples]

### A dashboard that is still loading [#a-dashboard-that-is-still-loading]

Labels you already know stay as text; only the numbers and the chart are placeholders. The figures use `ink`, so they hint at the coloured values on the way, and the chart uses a `gradient` texture that thickens towards the left.

```tsx title="examples/skeleton-dashboard.tsx"
import { Skeleton } from "@/components/dithercn/skeleton";

const TILES = ["Revenue", "Active users", "Churn"] as const;

export default function SkeletonDashboard() {
  return (
    <div aria-busy="true" className="flex w-full max-w-xl flex-col gap-3">
      <span className="sr-only">Loading dashboard</span>
      <div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
        {TILES.map((title, index) => (
          <div
            key={title}
            className="bg-background ring-border flex flex-col gap-3 rounded-xl p-4 ring-1"
          >
            {/* Real labels stay; only the numbers are still loading. */}
            <span className="text-muted-foreground text-xs">{title}</span>
            <Skeleton
              color="ink"
              className="h-7 w-24 rounded-[4px]"
              delay={index * 0.1}
            />
            <Skeleton
              className="h-2.5 w-16 rounded-[3px]"
              delay={index * 0.1 + 0.05}
            />
          </div>
        ))}
      </div>
      <div className="bg-background ring-border flex flex-col gap-4 rounded-xl p-4 ring-1">
        <span className="text-muted-foreground text-xs">
          Requests, last 30 days
        </span>
        <Skeleton
          color="ink"
          fill="gradient"
          className="h-36 w-full rounded-lg"
          delay={0.3}
        />
      </div>
    </div>
  );
}
```

### A table that is still loading [#a-table-that-is-still-loading]

Rows offset their `delay` so the sweep runs down the table as a wave instead of every row flashing at once.

```tsx title="examples/skeleton-table.tsx"
import { Skeleton } from "@/components/dithercn/skeleton";

const ROWS = [0, 1, 2, 3, 4];

export default function SkeletonTable() {
  return (
    <div
      aria-busy="true"
      className="bg-background ring-border w-full max-w-xl overflow-hidden rounded-xl ring-1"
    >
      <span className="sr-only">Loading members</span>
      <div className="text-muted-foreground grid grid-cols-[1fr_6rem_5rem] gap-4 border-b px-5 py-3 text-xs font-medium">
        <span>Member</span>
        <span>Role</span>
        <span className="text-right">Last seen</span>
      </div>
      {ROWS.map((row) => (
        <div
          key={row}
          className="grid grid-cols-[1fr_6rem_5rem] items-center gap-4 border-b px-5 py-3.5 last:border-b-0"
        >
          <div className="flex items-center gap-3">
            <Skeleton shape="circle" size={28} delay={row * 0.12} />
            <Skeleton
              className="h-3"
              style={{ width: `${48 + ((row * 17) % 30)}%` }}
              delay={row * 0.12}
            />
          </div>
          <Skeleton className="h-3 w-14" delay={row * 0.12 + 0.06} />
          <Skeleton className="ml-auto h-3 w-12" delay={row * 0.12 + 0.12} />
        </div>
      ))}
    </div>
  );
}
```

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `shape` | `"block" \| "text" \| "circle"` | `"block"` | A rectangle, lines of text, or an avatar. |
| `lines` | `number` | `3` | Text only. How many lines; the last one is shorter. |
| `size` | `number` | `40` | Circle only. Diameter in pixels. |
| `color` | `DitherColor` | `"grey"` | A palette name such as "ink" or "blue", a CSS variable name, or any CSS colour. |
| `fill` | `"gradient" \| "dotted" \| "hatched" \| "solid"` | `"gradient"` | Texture of the resting layer: a Bayer ramp from the left, a dot lattice, diagonals, or a plain tint. |
| `animation` | `"sweep" \| "pulse" \| "none"` | `"sweep"` | A dense band sweeping across, the dither stepping up and down through its levels, or nothing. |
| `cell` | `number` | `2` | Size of one dither cell in CSS pixels. |
| `duration` | `number` | `1.8` | Seconds for one sweep or pulse, including the pause after it. |
| `delay` | `number` | `0` | Seconds to wait before the first sweep. Offset it per row for a wave. |

## Notes [#notes]

* Skeletons are hidden from assistive technology. Put `aria-busy="true"` on the region that is loading and a visually hidden sentence such as "Loading members", as the examples do.
* With reduced motion, the sweep is removed and the pulse holds still; the texture stays, so the placeholder still reads as one.
* The sweep animates `clip-path` on masked layers, and the pulse switches masked layers on and off in steps. There is no JavaScript and no canvas, so a page full of skeletons costs one small shared `<style>` tag.