Installation
pnpm dlx shadcn@latest add https://asciicn.dev/r/ascii-button.jsonUsage
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.
filledshows the label row in inverse video: a slab of the colour with the label knocked out in the page colour.outlineshows the frame and a plain label, and inverts the row on hover.ghostis 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
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.
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.
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.
Props
| Prop | Type | Default |
|---|---|---|
| variant filled is an inverse-video label row. outline inverts on hover. ghost shows its frame only on hover and focus. | Type: "filled" | "outline" | "ghost" | Default "filled" |
| frame Box-drawing set for the frame. brackets draws only the four corners. | Type: "rounded" | "light" | "heavy" | "double" | "ascii" | "brackets" | Default "rounded" |
| color A palette name such as "green", a CSS variable name or any CSS colour. | Type: AsciiColor | Default "ink" |
| size Cells of 14, 16 and 20 pixels, so buttons 28, 32 and 40 pixels tall. icon fits one icon; give it an aria-label. | Type: "sm" | "md" | "lg" | "icon" | Default "md" |
| cursor Glyph shown before the label on hover and focus. | Type: string | Default "›" |
| loading Busy state: a spinner takes the cursor's cell, clicks are ignored and aria-busy is set. | Type: boolean | Default false |
| spinner Which SPINNERS sequence spins while loading. | Type: "braille" | "orbit" | "arc" | "bar" | "quadrant" | "tick" | Default "braille" |
| asChild Render your own single child, such as a link, with the button's look. | Type: boolean | Default false |
All other props pass through to the <button>, or to your child with asChild.
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-disabledon it and leave outhref. - The frame and row are text and CSS in one shared
<style>tag. Only the spinner runs JavaScript.