# Loader

A small dithered loading indicator: a ring with a comet tail, three dots that dissolve in turn, a pulse, or a sweeping bar.

> 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/loader.json](https://dithercn.dev/r/loader.json), under `files[].content`.

## Installation [#installation]

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

## Usage [#usage]

```tsx
import { Loader } from "@/components/dithercn/loader";

export function Saving() {
  return <Loader size="sm" color="blue" label="Saving changes" />;
}
```

Each variant is a tone field on a small grid of cells. Every frame, the loader thresholds the field with the Bayer matrix and paints the inked cells into a canvas at one pixel per cell. Shape edges are hard, so the ring and the dots keep clean outlines; only the tone inside them changes, so the motion happens in the dither. Cells are 2 pixels by default, big enough that you see the Bayer steps.

* `ring`: a thick ring with a comet on a faint track. The head is solid and the tail steps down through the dither levels behind it. The tail stretches and shrinks as it goes round, so the ring breathes instead of spinning at one pace.
* `pulse`: a dot that beats while three rings ripple out from it and thin out as they grow. Good for "waiting for something to arrive".
* `dots`: three discs that build up from sparse dither to solid and dissolve again, one after another. Twice as wide as it is tall.
* `bar`: a dense head easing across a faint track, its tail thinning behind it. Three times as wide as it is tall.

## Examples [#examples]

### Variants and sizes [#variants-and-sizes]

`sm`, `md` and `lg` are 24, 40 and 64 pixels tall. Cells default to 1 pixel at `sm` and 2 pixels above it, so larger loaders show more cells and smoother falloff.

```tsx title="examples/loader-variants.tsx"
import { Loader } from "@/components/dithercn/loader";
import type { LoaderSize, LoaderVariant } from "@/components/dithercn/loader";

const VARIANTS: readonly LoaderVariant[] = ["ring", "dots", "pulse", "bar"];
const SIZES: readonly LoaderSize[] = ["sm", "md", "lg"];

export default function LoaderVariants() {
  return (
    <div className="flex w-full max-w-xl flex-col gap-6">
      {VARIANTS.map((variant) => (
        <div
          key={variant}
          className="flex flex-col gap-3 sm:flex-row sm:items-center"
        >
          <span className="text-muted-foreground w-12 shrink-0 font-mono text-xs">
            {variant}
          </span>
          <div className="flex flex-wrap items-center gap-x-8 gap-y-4">
            {SIZES.map((size) => (
              <Loader
                key={size}
                variant={variant}
                size={size}
                label={`Loading, ${variant} ${size}`}
              />
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}
```

### In context [#in-context]

A ring next to a deploy, a bar under an upload, and a pulse in an empty event feed that is waiting for its first event. Give each one a `label` that says what is loading.

```tsx title="examples/loader-in-context.tsx"
import { Loader } from "@/components/dithercn/loader";

export default function LoaderInContext() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <div className="bg-background ring-border flex items-center gap-3 rounded-xl px-4 py-3 ring-1">
        <Loader size="sm" color="blue" label="Deploying" />
        <div className="flex min-w-0 flex-1 flex-col">
          <span className="text-sm font-medium">Deploying to production</span>
          <span className="text-muted-foreground truncate font-mono text-xs">
            main · 8e52937
          </span>
        </div>
        <span className="text-muted-foreground font-mono text-xs tabular-nums">
          0:42
        </span>
      </div>
      <div className="bg-background ring-border flex flex-col gap-3 rounded-xl p-4 ring-1">
        <div className="flex items-baseline justify-between">
          <span className="text-sm font-medium">Uploading assets</span>
          <span className="text-muted-foreground font-mono text-xs">
            128 of 342
          </span>
        </div>
        <Loader
          variant="bar"
          size="sm"
          color="green"
          label="Uploading assets"
        />
      </div>
      <div className="bg-background ring-border flex flex-col items-center gap-3 rounded-xl px-4 py-8 ring-1">
        <Loader
          variant="pulse"
          size="lg"
          color="purple"
          bloom="low"
          label="Waiting for the first event"
        />
        <div className="flex flex-col items-center gap-0.5 text-center">
          <span className="text-sm font-medium">Listening for events</span>
          <span className="text-muted-foreground text-xs">
            Send a test webhook to see it here.
          </span>
        </div>
      </div>
    </div>
  );
}
```

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"ring" \| "dots" \| "pulse" \| "bar"` | `"ring"` | Which animation to draw. |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Height of 24, 40 or 64 pixels. |
| `label` | `string` | `"Loading"` | What screen readers announce. |
| `color` | `DitherColor` | `"ink"` | A palette name such as "green", a CSS variable name, or any CSS colour. |
| `bloom` | `"off" \| "low" \| "high"` | `"off"` | A soft glow in the loader's colour. |
| `matrix` | `"bayer2" \| "bayer4" \| "bayer8" \| "blue-noise"` | `"bayer4"` | The threshold pattern. |
| `cell` | `number` | `2 (1 at sm)` | Size of one dither cell in CSS pixels. |
| `speed` | `number` | `1` | Speed multiplier. |

## Notes [#notes]

* The root has `role="status"` and a visually hidden `label`, so screen readers announce it politely when it appears. The canvas is hidden from them.
* With reduced motion turned on, each variant draws one still frame that still reads as a loader.
* It pauses when scrolled out of view or when the tab is hidden. A loader is at most about a thousand cells, so a frame is cheap, and it uses a 2D canvas rather than WebGL, so loaders never count against the page's WebGL budget.
* Cells snap to whole device pixels, so a loader stays crisp at any zoom level.