For the complete documentation index, see 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.

ASCII tabs

Tabs drawn as box-drawing folder tabs or a sliding glyph underline, with arrow-key navigation and a roving tab stop.

Controls
Variant
Active colour
Size

Installation

pnpm dlx shadcn@latest add https://asciicn.dev/r/ascii-tabs.json

Usage

import { AsciiTabs } from "@/components/asciicn/ascii-tabs";
 
export function ProjectTabs() {
  return (
    <AsciiTabs
      label="Project"
      tabs={[
        { value: "overview", label: "Overview", content: <Overview /> },
        { value: "deploys", label: "Deploys", content: <Deploys /> },
      ]}
    />
  );
}

Each tab has a value, a plain-text label and the content of its panel. Labels are plain strings because every tab is a whole number of glyph cells wide: the label plus a cell of space each side, with a divider cell between tabs.

The folder variant lifts the active tab out of a framed panel, like a file folder:

            ╭──────────╮
  Overview  │ Deploys  │ Settings
╭───────────╯          ╰──────────────╮
│                                     │

When you pick another tab, its lid and the opening under it slide over in whole cells for 200ms, and the sides close once they land.

The underline variant runs a light rule ─ under the tabs with a heavy ━ under the active one. It slides in half-cell steps, using ╾ and ╼ where it ends halfway through a cell.

The tabs control themselves, starting from defaultValue or the first tab. To control them, pass value and onValueChange.

Examples

Install command

The underline variant in green, switching between package managers.

Controlled

value and onValueChange from useState, at size="sm".

Props

PropTypeDefault
tabs*

{ value: string; label: string; content: ReactNode }, left to right.

Type: readonly AsciiTab[]Default –
value

The active tab, when you control it.

Type: stringDefault –
defaultValue

The tab active at first, when the tabs control themselves.

Type: stringDefault The first tab
onValueChange

Called with the new tab's value when the visitor picks a tab.

Type: (value: string) => voidDefault –
label

Names the tab list for screen readers.

Type: stringDefault –
variant

Folder tabs over a framed panel, or a sliding underline.

Type: "folder" | "underline"Default "folder"
color

Colour of the active label and the underline, a palette name or any CSS colour.

Type: AsciiColorDefault "ink"
size

Glyph cell height: 12, 16 or 20px.

Type: "sm" | "md" | "lg"Default "md"
cell

Glyph cell height in CSS pixels. Overrides size.

Type: numberDefault –

Notes

  • The tabs follow the ARIA tabs pattern. The list has one tab stop, on the active tab. Left and Right arrows move between tabs and wrap at the ends, Home and End jump to the first and last, and the panel follows focus.
  • Each panel is a tabpanel named by its tab and is a tab stop itself, so keyboard users can reach a panel with no controls in it. Inactive panels stay in the page with hidden.
  • Each tab's hit area reaches over the frame rows above and below its label, so a 16px row is still easy to tap.
  • The frame is DOM text in --ascii-font and hidden from screen readers. With reduced motion, the lid and the underline jump to the new tab.
  • The strip is as wide as its tabs. Keep labels short on narrow screens: three tabs of eight letters need about 34 cells.