# ASCII button

A real button set in glyphs: a box-drawing frame around a label row that inverts like a terminal selection, with a spinner while loading.

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

## Installation [#installation]

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

## Usage [#usage]

```tsx
import { AsciiButton } from "@/components/asciicn/ascii-button";

export function Actions() {
  return (
    <>
      <AsciiButton color="green">Deploy</AsciiButton>
      <AsciiButton variant="outline" frame="brackets">
        View logs
      </AsciiButton>
    </>
  );
}
```

`AsciiButton` renders a real `<button>` with `type="button"` unless you pass another `type`. It is set in the glyph font, two cells tall. Its frame is box-drawing text whose lines run along the button's edges, and the label sits on the row between them.

* `filled` shows the label row in inverse video: a slab of the colour with the label knocked out in the page colour.
* `outline` shows the frame and a plain label, and inverts the row on hover.
* `ghost` is a plain label whose frame and inverse row appear on hover and focus.

On hover and keyboard focus, a cursor glyph (`›`) steps into the cell before the label, as a terminal menu marks its selection. Press darkens the row.

The label, the gap between children and any icon are whole cells wide (an icon takes two), so the frame always closes on the grid.

## Examples [#examples]

### Variants, frames and colours [#variants-frames-and-colours]

Each row is one variant across four colours and a mix of frames: `rounded`, `light`, `heavy`, `double`, `ascii` and `brackets`, which draws only the corners.

```tsx title="examples/ascii-button-palette.tsx"
"use client";

import {
  ASCII_BUTTON_FRAMES,
  ASCII_BUTTON_VARIANTS,
  AsciiButton,
} from "@/components/asciicn/ascii-button";

const COLORS = ["ink", "green", "orange", "red"] as const;

export default function AsciiButtonPalette() {
  return (
    <div className="flex flex-col items-center gap-6">
      {ASCII_BUTTON_VARIANTS.map((variant) => (
        <div key={variant} className="flex flex-wrap justify-center gap-4">
          {COLORS.map((color, index) => (
            <AsciiButton
              key={color}
              variant={variant}
              color={color}
              frame={
                ASCII_BUTTON_FRAMES[
                  (index + ASCII_BUTTON_VARIANTS.indexOf(variant)) %
                    ASCII_BUTTON_FRAMES.length
                ]
              }
            >
              {variant}
            </AsciiButton>
          ))}
        </div>
      ))}
    </div>
  );
}
```

### Loading in a form [#loading-in-a-form]

`loading` keeps the label and the width, spins a `SPINNERS` sequence in the cursor's cell, sets `aria-busy` and ignores clicks until it clears. Submit this one to see it.

```tsx title="examples/ascii-button-form.tsx"
"use client";

import { useState } from "react";
import type { FormEvent } from "react";

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

export default function AsciiButtonForm() {
  const [saving, setSaving] = useState(false);
  const [saved, setSaved] = useState(false);

  const submit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    setSaving(true);
    setSaved(false);
    window.setTimeout(() => {
      setSaving(false);
      setSaved(true);
    }, 1800);
  };

  return (
    <form onSubmit={submit} className="flex w-full max-w-sm flex-col gap-4">
      <label className="flex flex-col gap-1.5 font-mono text-sm">
        <span className="text-muted-foreground">Project name</span>
        <input
          name="project"
          defaultValue="northwind-api"
          autoComplete="off"
          className="h-9 rounded-md border bg-background px-3 text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
        />
      </label>
      <div className="flex items-center justify-end gap-4">
        <span role="status" className="font-mono text-xs text-muted-foreground">
          {saved ? "saved" : ""}
        </span>
        <AsciiButton type="button" variant="ghost" size="sm" disabled={saving}>
          Cancel
        </AsciiButton>
        <AsciiButton type="submit" size="sm" loading={saving} spinner="orbit">
          {saving ? "Saving" : "Save"}
        </AsciiButton>
      </div>
    </form>
  );
}
```

### As a link [#as-a-link]

Pass `asChild` and a single child, such as Next.js `Link` or a plain `<a>`, and the child gets the button's frame and states. It stays a link: right click and middle click work, and the router handles navigation.

```tsx title="examples/ascii-button-link.tsx"
import Link from "next/link";

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

export default function AsciiButtonLink() {
  return (
    <div className="flex w-full max-w-md flex-col items-center gap-6 text-center">
      <div className="flex flex-col gap-2">
        <span className="font-display text-2xl font-semibold tracking-tight text-balance">
          Glyph components for your next launch
        </span>
        <span className="text-sm text-muted-foreground">
          Copy them in with the shadcn CLI. Own the code.
        </span>
      </div>
      <div className="flex flex-wrap justify-center gap-4">
        <AsciiButton asChild size="lg">
          <Link href="/docs/installation">Install asciicn</Link>
        </AsciiButton>
        <AsciiButton asChild size="lg" variant="outline" frame="brackets">
          <Link href="/docs/examples">See the examples</Link>
        </AsciiButton>
      </div>
    </div>
  );
}
```

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"filled" \| "outline" \| "ghost"` | `"filled"` | filled is an inverse-video label row. outline inverts on hover. ghost shows its frame only on hover and focus. |
| `frame` | `"rounded" \| "light" \| "heavy" \| "double" \| "ascii" \| "brackets"` | `"rounded"` | Box-drawing set for the frame. brackets draws only the four corners. |
| `color` | `AsciiColor` | `"ink"` | A palette name such as "green", a CSS variable name or any CSS colour. |
| `size` | `"sm" \| "md" \| "lg" \| "icon"` | `"md"` | Cells of 14, 16 and 20 pixels, so buttons 28, 32 and 40 pixels tall. icon fits one icon; give it an aria-label. |
| `cursor` | `string` | `"›"` | Glyph shown before the label on hover and focus. |
| `loading` | `boolean` | `false` | Busy state: a spinner takes the cursor's cell, clicks are ignored and aria-busy is set. |
| `spinner` | `"braille" \| "orbit" \| "arc" \| "bar" \| "quadrant" \| "tick"` | `"braille"` | Which SPINNERS sequence spins while loading. |
| `asChild` | `boolean` | `false` | Render your own single child, such as a link, with the button's look. |

All other props pass through to the `<button>`, or to your child with `asChild`.

## Notes [#notes]

* Keyboard focus shows a 2 pixel outline outside the frame and the cursor glyph, so keyboard users see the same marker as pointer users.
* Hover styles only apply on devices that can hover. Press works everywhere, so touch screens get the darker row as feedback.
* With reduced motion, the row switches without fading, the press does not scale and the spinner holds its first frame.
* Disabled and loading buttons drop to 45% opacity with a not-allowed or progress cursor. For a disabled link, set `aria-disabled` on it and leave out `href`.
* The frame and row are text and CSS in one shared `<style>` tag. Only the spinner runs JavaScript.