# Skeleton

Loading placeholders for text, blocks and avatars made of shade glyphs, with a band of denser glyphs that sweeps across the page.

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

## Installation [#installation]

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

## Usage [#usage]

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

export function PostLoading() {
  return (
    <div aria-busy="true" className="flex flex-col gap-3">
      <span className="sr-only">Loading post</span>
      <Skeleton rows={6} />
      <Skeleton shape="text" lines={3} />
    </div>
  );
}
```

A skeleton is rows of `░` the width of its container. Set the width with a class such as `w-32`; the height is `rows` cells, so it lines up with the glyph grid. `shape="text"` draws `lines` rows with a gap between them and a shorter last line, and `shape="circle"` draws a disc `size` rows across for avatars.

While it waits, a band of `▒▓▒` sweeps from left to right. The band's position comes from the viewport and a shared clock, so the sweep passes over every skeleton on the page as one light rather than each one starting on its own. `motion="pulse"` steps every glyph between `░` and `▒` instead. Swap the glyphs with `chars`, from resting to peak: `"·•●"` makes a dot skeleton, `"⠂⠕⠿"` a Braille one.

## Examples [#examples]

### Cards and a panel [#cards-and-a-panel]

Each card keeps its real label while its value loads. The wide panel below uses `chars="·•●"`, so it sweeps in dots instead of shades.

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

const TILES = ["Build", "Tests", "Deploy"] as const;

export default function SkeletonPanel() {
  return (
    <div aria-busy="true" className="flex w-full max-w-xl flex-col gap-3">
      <span className="sr-only">Loading the pipeline</span>
      <div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
        {TILES.map((title) => (
          <div
            key={title}
            className="bg-background flex flex-col gap-3 rounded-lg border p-4"
          >
            {/* Real labels stay; only the numbers are still loading. */}
            <span className="text-muted-foreground text-xs">{title}</span>
            <Skeleton color="ink" rows={2} cell={14} className="w-24" />
            <Skeleton rows={1} className="w-16" />
          </div>
        ))}
      </div>
      <div className="bg-background flex flex-col gap-4 rounded-lg border p-4">
        <span className="text-muted-foreground text-xs">Build log</span>
        <Skeleton color="ink" chars="·•●" rows={10} />
      </div>
    </div>
  );
}
```

### Table [#table]

Rows of a member table: a two-row disc for the avatar and single-row lines of different widths. One band crosses every cell of the table in step.

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

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

export default function SkeletonTable() {
  return (
    <div
      aria-busy="true"
      className="bg-background w-full max-w-xl overflow-hidden rounded-lg border"
    >
      <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 last:border-b-0"
        >
          <div className="flex items-center gap-3">
            <Skeleton shape="circle" size={2} />
            <Skeleton
              rows={1}
              style={{ width: `${48 + ((row * 17) % 30)}%` }}
            />
          </div>
          <Skeleton rows={1} className="w-14" />
          <Skeleton rows={1} className="ml-auto w-12" />
        </div>
      ))}
    </div>
  );
}
```

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `shape` | `"block" \| "text" \| "circle"` | `"block"` | A filled rectangle, lines of text, or a disc. |
| `rows` | `number` | `3` | block: height in rows of cells. |
| `lines` | `number` | `3` | text: number of lines. The last one is shorter. |
| `size` | `number` | `3` | circle: diameter in rows of cells. |
| `motion` | `"sweep" \| "pulse" \| "none"` | `"sweep"` | A dense band sweeping across, every glyph pulsing, or still. |
| `chars` | `string` | `"░▒▓"` | Glyphs from resting to the peak of the sweep. |
| `color` | `AsciiColor` | `"grey"` | Glyph colour, a palette name or any CSS colour. |
| `cell` | `number` | `--ascii-cell` | Glyph cell height in CSS pixels. |

## Notes [#notes]

* Skeletons are `aria-hidden`. Mark the region that is loading with `aria-busy="true"` and give it a short status in text, such as a visually hidden "Loading post".
* The first render, on the server too, is plain rows of the resting glyph, so there is no flash and no layout shift when the sweep starts.
* The sweep redraws at 30fps, writing text straight into each line, and pauses offscreen and in hidden tabs. With reduced motion it holds the resting glyph.