# ASCII box

A box-drawing frame that wraps any content and snaps to the glyph cell grid, with labels in its borders and a shade-glyph shadow.

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

## Installation [#installation]

```bash
npx shadcn@latest add https://asciicn.dev/r/ascii-box.json
```

## Usage [#usage]

```tsx
import { AsciiBox } from "@/components/asciicn/ascii-box";

export function BuildLog() {
  return (
    <AsciiBox title="build #4182" footer="exit 0" frame="rounded">
      <p>compiled 214 modules in 1.8s</p>
    </AsciiBox>
  );
}
```

`AsciiBox` draws its frame as real box-drawing text in the glyph font (`--ascii-font`): one cell for each corner and each side, in `light` (`┌─┐`), `heavy` (`┏━┓`), `double` (`╔═╗`), `rounded` (`╭─╮`) or `ascii` (`+-+`). The lines run through the middle of the outer cells, so the frame looks the same at any zoom and in any theme.

After it mounts, the box measures its content, rounds its own width and height up to whole cells and pins them, so every glyph of the frame sits on the cell grid and the corners meet the sides exactly. It measures again whenever the content or the space around it changes size. When rounding up would overflow its container, it rounds down instead. Before it measures, and during server rendering, the frame is already drawn and closed.

By default the box is as wide as its content (`w-fit`) and never wider than its container. Pass `className="w-full"` to fill the container. `padding` is in rows, and the columns are doubled, because a glyph cell is about half as wide as it is tall.

`title` and `footer` are set into the borders as text, like `┌─ build #4182 ───┐`. A `title` also names the box as a `group` for screen readers. Long labels are cut short with an ellipsis rather than breaking the frame.

## Examples [#examples]

### Nested panels [#nested-panels]

Boxes inside a box, laid out with CSS grid. Each inner box fills its grid cell with `w-full` and snaps to whole cells inside it. The gap is one cell (`var(--box-ch)`).

```tsx title="examples/ascii-box-panels.tsx"
import { AsciiBox } from "@/components/asciicn/ascii-box";

const PROCESSES = [
  ["node", "212", "38.2%"],
  ["postgres", "88", "12.7%"],
  ["redis", "41", "3.1%"],
] as const;

export default function AsciiBoxPanels() {
  return (
    <AsciiBox
      title="htop · prod-eu-1"
      frame="heavy"
      className="w-full max-w-2xl"
    >
      <div className="grid gap-[var(--box-ch)] sm:grid-cols-2">
        <AsciiBox title="cpu" color="green" padding={0} className="w-full">
          <p className="font-mono text-sm">
            <span className="text-[var(--ascii-color-green)]">████████▌</span>
            <span className="text-muted-foreground">░░░░░ 62%</span>
          </p>
        </AsciiBox>
        <AsciiBox title="memory" color="blue" padding={0} className="w-full">
          <p className="font-mono text-sm">
            <span className="text-[var(--ascii-color-blue)]">█████▍</span>
            <span className="text-muted-foreground">░░░░░░░░ 41%</span>
          </p>
        </AsciiBox>
        <AsciiBox
          title="processes"
          footer="3 of 118"
          labelAlign="end"
          padding={0}
          className="w-full sm:col-span-2"
        >
          <table className="w-full font-mono text-sm">
            <thead className="text-muted-foreground">
              <tr>
                <th className="text-left font-normal">name</th>
                <th className="text-right font-normal">pid</th>
                <th className="text-right font-normal">cpu</th>
              </tr>
            </thead>
            <tbody>
              {PROCESSES.map(([name, pid, cpu]) => (
                <tr key={name}>
                  <td>{name}</td>
                  <td className="text-right">{pid}</td>
                  <td className="text-right">{cpu}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </AsciiBox>
      </div>
    </AsciiBox>
  );
}
```

### Dialog [#dialog]

A `double` frame with a centred title, a `medium` shade shadow, and `filled` so it covers whatever sits behind it. The buttons are [`AsciiButton`](/docs/interface/ascii-button).

```tsx title="examples/ascii-box-dialog.tsx"
import { AsciiBox } from "@/components/asciicn/ascii-box";
import { AsciiButton } from "@/components/asciicn/ascii-button";

export default function AsciiBoxDialog() {
  return (
    <AsciiBox
      role="alertdialog"
      aria-describedby="ascii-box-dialog-body"
      title="Delete branch?"
      labelAlign="center"
      frame="double"
      shadow="medium"
      color="red"
      filled
      padding={2}
      className="max-w-md"
    >
      <p
        id="ascii-box-dialog-body"
        className="text-sm text-balance text-muted-foreground"
      >
        <code className="font-mono text-foreground">feat/glyph-atlas</code> has
        14 commits that are not on main. They will be gone for good.
      </p>
      <div className="mt-[var(--box-ch)] flex justify-end gap-4">
        <AsciiButton variant="ghost" size="sm" color="grey">
          Keep it
        </AsciiButton>
        <AsciiButton size="sm" color="red" frame="heavy">
          Delete
        </AsciiButton>
      </div>
    </AsciiBox>
  );
}
```

### Sizes to its content [#sizes-to-its-content]

Add lines and the box grows to fit them, one whole cell at a time.

```tsx title="examples/ascii-box-fit.tsx"
"use client";

import { useState } from "react";

import { AsciiBox } from "@/components/asciicn/ascii-box";
import { AsciiButton } from "@/components/asciicn/ascii-button";

const LINES = [
  "$ asciicn add ascii-box",
  "✓ resolved 2 items",
  "✓ wrote components/asciicn/ascii-box.tsx",
  "✓ wrote lib/ascii/index.ts",
  "done in 0.42s",
];

export default function AsciiBoxFit() {
  const [count, setCount] = useState(2);
  return (
    <div className="flex flex-col items-center gap-6">
      <AsciiBox
        title="stdout"
        footer={`${count} lines`}
        frame="rounded"
        className="font-mono text-sm"
      >
        {LINES.slice(0, count).map((line) => (
          <p key={line}>{line}</p>
        ))}
      </AsciiBox>
      <AsciiButton
        variant="outline"
        size="sm"
        onClick={() => setCount((value) => (value % LINES.length) + 1)}
      >
        {count === LINES.length ? "Reset" : "Next line"}
      </AsciiButton>
    </div>
  );
}
```

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `frame` | `"light" \| "heavy" \| "double" \| "rounded" \| "ascii"` | `"light"` | Box-drawing set for the frame. |
| `title` | `ReactNode` |  | A label set into the top border. Also names the box for screen readers. |
| `footer` | `ReactNode` |  | A label set into the bottom border. |
| `labelAlign` | `"start" \| "center" \| "end"` | `"start"` | Where the labels sit along their border. |
| `shadow` | `boolean \| "light" \| "medium" \| "dark"` | `false` | A drop shadow of ░, ▒ or ▓ one cell right and one row down. true is light. The box reserves the space, so the shadow never overlaps its neighbours. |
| `color` | `AsciiColor` | `"ink"` | Frame colour: a palette name, a CSS variable name or any CSS colour. |
| `padding` | `number` | `1` | Space between the frame and the content in rows. Columns are doubled. |
| `cell` | `number` | `--ascii-cell (12)` | Cell height in CSS pixels. |
| `filled` | `boolean` | `false` | Paint --ascii-paper inside the frame, so the box hides what sits behind it. |
| `backdrop` | `ReactNode` |  | Decoration drawn inside the frame and behind the content, such as AsciiGlow. It fills the inner cells exactly. |

Other props pass through to the wrapping `<div>`. Inside the box, `var(--box-ch)` and `var(--box-cw)` hold the cell height and width, for spacing content on the same grid.

## Notes [#notes]

* The frame glyphs are `aria-hidden` and can't be selected, so copying the box copies only its content and labels.
* Snapping uses a `ResizeObserver` and a `MutationObserver` on the content. Nothing runs between changes.
* The glyph font needs box-drawing coverage for the frame to join up. The `ascii-tokens` font stack has it.
* `AsciiCard` and other asciicn pieces are built on this box.