Skip to content

UI components

The ascii-things/ui entry is an optional React layer around the scenes. It is not a design system: it is a composable set of primitives for the chrome a scene usually needs — framed stages, command inputs, live readouts, scene tabs, panels, toolbars, status readouts, and ready-made scene heroes.

It has no runtime dependencies of its own. It expects React 18+.

ROBOT DOG / TEST YARD

A small scene.
A very good dog.

The full recipe: a live engine, a framed stage, a command input, and a live readout. Type a command and the dog fetches the ball.

yard@04 · awaiting commandUNIT 01 · IDLE · BALL READY
LIVE
import "ascii-things/ui.css";

All components use the same CSS custom properties. Containers are square by default; set --ascii-radius on a wrapper if your host needs rounding. Import the stylesheet from your app shell or root layout, not from every component.

A framed scene surface with HUD slots.

import { AsciiDog } from "ascii-things/react";
import { AsciiStage } from "ascii-things/ui";
<AsciiStage
label="ROBOT DOG / TEST YARD"
status="LIVE"
aspectRatio="4 / 3"
footer={<span>footer content</span>}
overlay={<span>optional overlay</span>}
>
<AsciiDog palette="abyss" seed={7} />
</AsciiStage>;
Prop Type Default
label ReactNode — Top-left HUD label
status ReactNode — Top-right HUD status; adds a live dot
overlay ReactNode — Absolutely positioned center overlay
footer ReactNode — Caption below the canvas
aspectRatio string | number "16 / 10" CSS aspect-ratio for the frame
ariaLabel string — Accessible name for the <figure>
className string — Host class
style CSSProperties — Host style
children ReactNode — The scene canvas

AsciiStage gives its frame an aspect ratio, which solves the collapsed-canvas problem for many layouts. The canvas inside still fills 100% of the frame.

An accessible command input with an inline action.

import { AsciiPrompt } from "ascii-things/ui";
<AsciiPrompt
label="Tell the dog what to fetch"
placeholder="Throw the ball…"
actionLabel="Throw"
status="Awaiting command"
hint="Enter submits · focus stays"
onSubmit={(value) => console.log(value)}
/>;
Prop Type Default
label string "Command" Screen-reader label
placeholder string "Type a command…" Input placeholder
actionLabel string "Send" Submit button text
status ReactNode — Rendered in a polite live region
hint ReactNode — Helper text
value string — Controlled value
defaultValue string "" Initial uncontrolled value
onValueChange (value) => void — Fires on every edit
onSubmit (value) => void — Fires with the trimmed value
maxLength number 80 Input limit
disabled boolean false Disable input and action
autoFocus boolean false Focus the input on mount
className string — Host class
style CSSProperties — Host style

Uncontrolled by default. Enter submits, the input clears, and focus stays in the field. Controlled when you pass value.

A compact label/value grid for live metrics.

import { AsciiReadout } from "ascii-things/ui";
<AsciiReadout
ariaLabel="Live scene statistics"
columns={4}
items={[
{ label: "CELLS", value: "8,640" },
{ label: "UNITS", value: "01" },
{ label: "STATE", value: "SNIFF" },
{ label: "FPS", value: "60" },
]}
/>;
Prop Type Default
items { label, value }[] — Rows; returns null when empty
columns number 4 CSS grid columns
ariaLabel string — Accessible name for the <dl>
className string — Host class
style CSSProperties — Host style

Keyboard-navigable tabs for switching scenes, palettes, or variants.

import { useState } from "react";
import type { PaletteName } from "ascii-things";
import { AsciiSceneTabs } from "ascii-things/ui";
const [palette, setPalette] = useState<PaletteName>("abyss");
<AsciiSceneTabs
ariaLabel="Palette"
value={palette}
onChange={(id) => setPalette(id as PaletteName)}
scenes={[
{ id: "abyss", label: "Abyss", meta: "night" },
{ id: "paper", label: "Paper", meta: "day" },
{ id: "neon", label: "Neon", meta: "loud" },
{ id: "mono", label: "Mono", meta: "quiet" },
]}
/>;
Prop Type Default
scenes { id, label, meta? }[] — Tab definitions
value string — Selected id
onChange (id) => void — Selection callback
ariaLabel string "Scenes" Tab list label
className string — Host class
style CSSProperties — Host style

Arrow keys, Home, and End move the selection. Roving tabIndex keeps one tab in the tab order.

The copy-paste recipe: dog scene, stage, prompt, live stats, and ball throws.

import { AsciiDogHero } from "ascii-things/ui";
import "ascii-things/ui.css";
export function Hero() {
return <AsciiDogHero palette="abyss" seed={7} />;
}

AsciiDogHero extends every AsciiDogOptions prop and adds:

Prop Type Default
eyebrow ReactNode "ROBOT DOG / TEST YARD" Kicker above the title
title ReactNode “A small scene. / A very good dog.” Headline
lede ReactNode Built-in copy Paragraph
promptLabel string "Tell the dog what to fetch" Input label
placeholder string "Tell the dog what to fetch…" Input placeholder
actionLabel string "Throw" Submit label
stageLabel string "ROBOT DOG / TEST YARD" Stage HUD label
onCommand (command) => void — Called after each command
className string — Host class
style CSSProperties — Host style

It sets label to "A live ASCII robot dog test yard" unless you pass one.

useAsciiScene owns the canvas lifecycle, live option updates, ready handle, and stats sampling for any scene. Use it when the built-in heroes are too opinionated.

import { createDog } from "ascii-things/dog";
import { useAsciiScene } from "ascii-things/ui";
function Scene() {
const { canvasRef, scene, stats } = useAsciiScene({
create: createDog,
options: { palette: "auto", seed: 7 },
onReady: (handle) => handle.trigger("throwBall", 240, 180),
});
return <canvas ref={canvasRef} aria-label="A live ASCII dog" />;
}

The hook returns canvasRef, the current scene handle, the latest stats, and a manual refresh() function. remountKey forces a fresh world when structural options change.

<AsciiButton variant="solid" size="md">Wake</AsciiButton>
<AsciiButton variant="ghost">Settle</AsciiButton>
<AsciiButton variant="quiet" size="sm">Reset</AsciiButton>

Variants: solid, ghost, quiet. Sizes: sm, md. The component forwards button attributes and a ref.

<AsciiToolbar label="Scene controls">
<AsciiButton>Wake</AsciiButton>
<AsciiToggle checked={calm} onCheckedChange={setCalm} label="Calm mode" />
</AsciiToolbar>

A role="toolbar" flex row for related controls. Pass label for an accessible name.

<AsciiToggle
checked={calm}
onCheckedChange={setCalm}
label="Calm mode"
hint="Settles the scene"
/>

A real role="switch" button with aria-checked, a label, and optional hint. The track and thumb stay circular even though containers are square.

<AsciiPanel
eyebrow="Control surface"
heading="Calm the yard."
actions={<AsciiStatus label="Mode" value="LIVE" tone="success" pulse live />}
footer="Square containers. Round indicators. One policy."
>
<AsciiToolbar label="Scene controls">...</AsciiToolbar>
</AsciiPanel>

padding accepts none, sm, md, or lg. Header, actions, body, and footer are all optional.

<AsciiStatus label="Scene" value="Robot dog" tone="accent" />
<AsciiStatus label="Motion" value="Running" tone="success" pulse live />

Tones: neutral, accent, success, warning. pulse animates the dot; live adds aria-live="polite".

<AsciiDivider />
<AsciiDivider label="State" />
<AsciiDivider orientation="vertical" />

Horizontal by default. Vertical dividers work inside toolbars and flex rows.

AsciiSceneHero is the generic version of the dog hero. It selects the right engine, action, copy, readout, and stage for any scene.

import { AsciiSceneHero } from "ascii-things/ui";
<AsciiSceneHero
scene="sharks"
options={{ palette: "abyss", seed: 11, density: 0.8 }}
eyebrow="PRESET / SHARKS"
title={<>One hero. <em>Any habitat.</em></>}
/>

The prompt action maps to splash for sharks, dispatch for robots, shockwave for jellyfish, and throwBall for the dog. onCommand receives the submitted text; onReady receives the scene handle.

Presets are available when you want the default copy:

import { AsciiSharkHero, AsciiRobotHero, AsciiJellyfishHero } from "ascii-things/ui";
<AsciiSharkHero options={{ palette: "paper", seed: 3 }} />
<AsciiRobotHero options={{ palette: "neon", count: 6 }} />
<AsciiJellyfishHero options={{ palette: "abyss", count: 4 }} />

AsciiDogHero remains available with its own dog-specific copy and ball interaction.

The dog hero is only a recipe. Every piece is exported separately.

import { useState } from "react";
import { AsciiDog } from "ascii-things/react";
import {
AsciiPrompt,
AsciiReadout,
AsciiSceneTabs,
AsciiStage,
} from "ascii-things/ui";
import "ascii-things/ui.css";
import type { PaletteName } from "ascii-things";
const PALETTES = [
{ id: "abyss", label: "Abyss", meta: "night" },
{ id: "paper", label: "Paper", meta: "day" },
{ id: "neon", label: "Neon", meta: "loud" },
{ id: "mono", label: "Mono", meta: "quiet" },
] as const;
export function Yard() {
const [palette, setPalette] = useState<PaletteName>("abyss");
return (
<div>
<AsciiSceneTabs
scenes={PALETTES.map((item) => ({ ...item }))}
value={palette}
onChange={(id) => setPalette(id as PaletteName)}
/>
<AsciiStage
label="ROBOT DOG / TEST YARD"
status="LIVE"
aspectRatio="4 / 3"
footer={
<AsciiReadout
items={[
{ label: "SCENE", value: "DOG" },
{ label: "PALETTE", value: palette.toUpperCase() },
]}
/>
}
>
<AsciiDog palette={palette} seed={7} />
</AsciiStage>
<AsciiPrompt
label="Tell the dog what to fetch"
placeholder="Throw the ball…"
actionLabel="Throw"
status="Awaiting command"
onSubmit={(command) => console.log(command)}
/>
</div>
);
}
LIVE

Click to throw the ball, or type a command below.

yard@04 · awaiting commandEnter submits · Click to throw the ball, or type a command below.

Seed 7, columns 110, and speed 1 are fixed in this demo. The palette and calm controls update the running engine without rebuilding the world.

The UI layer uses CSS variables, not Tailwind. Set them on :root or on a wrapper:

:root {
--ascii-accent: #ff7a1a;
--ascii-bg: #070809;
--ascii-panel: #0b0d0f;
--ascii-ink: #ece8df;
--ascii-muted: rgba(236, 232, 223, 0.56);
--ascii-faint: rgba(236, 232, 223, 0.32);
--ascii-line: rgba(236, 232, 223, 0.14);
--ascii-line-soft: rgba(236, 232, 223, 0.08);
--ascii-accent-soft: rgba(255, 122, 26, 0.14);
--ascii-font: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
--ascii-radius: 0;
}

These variables style the frame, prompt, readout, tabs, and hero. They do not recolor canvas glyphs. Canvas colors come from the scene palette. See Theming.

The UI layer is intentionally plain. If you already have a design system:

  • Keep AsciiStage for the frame and aspect ratio, and pass your own controls through overlay or below it.
  • Use your own Input and Button instead of AsciiPrompt.
  • Keep AsciiReadout as a small <dl> or render stats in your own layout.
  • Use AsciiDog directly if you do not need the frame at all.

Because the components are copy-paste friendly React source, they work beside shadcn/ui, Radix, or a hand-rolled component set without adapter code.