# Loader

A loading indicator drawn in glyphs: a spinner from six glyph sequences, walking dots, an eighth-block wave or a Braille comet.

> 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 asciicn agent skill, for building any ASCII effect on the engine, is at [/.well-known/agent-skills/asciicn/SKILL.md](/.well-known/agent-skills/asciicn/SKILL.md).

> **This is an example, not the only option.** It is one component built on the asciicn 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 `AsciiShader` ([Write your own](/docs/engine/write-your-own.md)), or draw a CPU grid with `AsciiGrid`, and read this example's source for technique. Its full source is in [https://asciicn.dev/r/loader.json](https://asciicn.dev/r/loader.json), under `files[].content`.

## Installation [#installation]

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

## Usage [#usage]

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

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

The default is a one-cell spinner that cycles the `orbit` sequence, `⣾⣽⣻⢿⡿⣟⣯⣷`. Pick another with `spinner`: `braille` (`⠋⠙⠹⠸`), `arc` (`◜◠◝◞`), `tick` (`-\|/`), `bar` (`▁▂▃▄▅▆▇█`) or `quadrant` (`▖▘▝▗`). Other variants are wider:

* `dots`: a bright `●` steps along three dots, leaving a half-lit `•` behind it.
* `wave`: six eighth blocks ride one sine wave, each a step behind its neighbour.
* `comet`: a Braille comet with a full head and a tail of single dots crosses six cells.

Inside a button, pass `color="currentColor"` so the glyphs follow the button's text.

## Examples [#examples]

### Every sequence [#every-sequence]

All six spinners and the three wider variants at `size="lg"`.

```tsx title="examples/loader-variants.tsx"
import { Loader } from "@/components/asciicn/loader";
import type { LoaderProps } from "@/components/asciicn/loader";

const ROWS: readonly { name: string; props: LoaderProps }[] = [
  { name: "orbit", props: { spinner: "orbit" } },
  { name: "braille", props: { spinner: "braille" } },
  { name: "arc", props: { spinner: "arc" } },
  { name: "tick", props: { spinner: "tick" } },
  { name: "bar", props: { spinner: "bar" } },
  { name: "quadrant", props: { spinner: "quadrant" } },
  { name: "dots", props: { variant: "dots" } },
  { name: "wave", props: { variant: "wave" } },
  { name: "comet", props: { variant: "comet" } },
];

export default function LoaderVariants() {
  return (
    <ul className="grid w-full max-w-lg grid-cols-3 gap-px overflow-hidden rounded-lg border bg-[var(--border)]">
      {ROWS.map((row) => (
        <li
          key={row.name}
          className="bg-background flex flex-col items-center gap-2 px-3 py-5"
        >
          <Loader size="lg" label={`Loading, ${row.name}`} {...row.props} />
          <span className="text-muted-foreground font-mono text-xs">
            {row.name}
          </span>
        </li>
      ))}
    </ul>
  );
}
```

### In context [#in-context]

A deploy row with a spinner, a log stream with a small wave and its label shown, and an empty event feed with a comet and a disabled button that holds a spinner.

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

export default function LoaderInContext() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <div className="bg-background flex items-center gap-3 rounded-lg border px-4 py-3">
        <Loader 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 flex items-center justify-between gap-3 rounded-lg border px-4 py-3 font-mono text-xs">
        <Loader
          variant="wave"
          size="sm"
          color="green"
          label="Streaming logs"
          showLabel
        />
        <span className="text-muted-foreground tabular-nums">1,204 lines</span>
      </div>
      <div className="bg-background flex flex-col items-center gap-4 rounded-lg border px-4 py-8">
        <Loader
          variant="comet"
          size="lg"
          color="purple"
          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>
        <Button size="sm" variant="outline" disabled>
          <Loader size="sm" color="currentColor" label="Sending" />
          Sending test event
        </Button>
      </div>
    </div>
  );
}
```

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | `"Loading"` | What is loading. Screen readers announce it politely. |
| `showLabel` | `boolean` | `false` | Show the label as text next to the glyphs. |
| `variant` | `"spinner" \| "dots" \| "wave" \| "comet"` | `"spinner"` | The animation. |
| `spinner` | `"orbit" \| "braille" \| "arc" \| "tick" \| "bar" \| "quadrant"` | `"orbit"` | For the spinner variant: which glyph sequence to cycle. |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Glyph cell height: 12, 16 or 24px. |
| `color` | `AsciiColor` | `"ink"` | Glyph colour, a palette name or any CSS colour such as "currentColor". |
| `speed` | `number` | `1` | Playback speed multiplier. 0 holds a still frame. |
| `cell` | `number` |  | Glyph cell height in CSS pixels. Overrides size. |

## Notes [#notes]

* The root is a polite live region (`role="status"`), so a screen reader reads `label` once when the loader appears. The glyphs are `aria-hidden`.
* The width is fixed in cells (1 for a spinner, 3 for dots, 6 for the wave and comet), so text next to it never shifts as frames change.
* Frames are written straight into the text node, only when the glyphs change, with no React render per frame. The loop pauses offscreen and in hidden tabs.
* With reduced motion the loader holds one frame chosen to read as busy on its own: a full orbit glyph, the middle dot lit, a wave crest, the comet mid-flight.