.cursor/skills/ink-tui/SKILL.md
Build beautiful, interactive terminal wizard interfaces using Ink (React for CLIs).
Ink is the dominant Node.js TUI framework — used by Claude Code (Anthropic), Gemini CLI (Google), GitHub Copilot CLI, Cloudflare Wrangler, Shopify CLI, Prisma, and many others.
This skill follows a reactive session-driven pattern: the rendered screen is a pure function of session state. Business logic sets state through store setters. The router derives which screen should be active. Nobody imperatively pushes screens around.
See references/ARCHITECTURE.md for the full reactive architecture: session, router, store, screen resolution, overlays, and data flow.
src/lib/wizard-session.ts) — single source of truth for all wizard decisionssrc/ui/tui/router.ts) — declarative flow pipelines with isComplete predicates per screensrc/ui/tui/store.ts) — nanostores-backed reactive store with explicit setters that trigger React re-renders via useSyncExternalStoresrc/ui/wizard-ui.ts) — interface bridging business logic to store; implemented by InkUI (TUI) and LoggingUI (CI)src/ui/tui/screen-registry.tsx) — factory function mapping screen names to components (App.tsx never changes)src/ui/tui/services/) — injected into screens via props (no dynamic imports in React components)src/ui/tui/screens/Screen enum in router.tsFlowEntry to the flow array with an isComplete predicatescreen-registry.tsxNo other files change.
Two patterns depending on the data:
WizardSession, add setter to WizardStore that calls emitChange(), add method to WizardUI interface + both implementationsWizardStore, add getter + setter, add method to WizardUI interface + both implementationsRead store.ts for examples of both patterns.
The project has reusable layout primitives in src/ui/tui/primitives/.
Always use these instead of building from scratch.
All primitives are barrel-exported from src/ui/tui/primitives/index.ts.
See references/PRIMITIVES.md for the catalog.
Read each primitive's source file for its current props interface.
Shared style constants (Colors, Icons, HAlign, VAlign) live in
src/ui/tui/styles.ts.
Playground: Run pnpm try --playground to see all primitives in action.
All state comparisons use TypeScript enums — no string literals. See the source files for current values:
Screen, Overlay, Flow — in router.tsRunPhase, OutroKind — in wizard-session.tsTaskStatus — in wizard-ui.tsink # Core: React renderer for terminals (uses Yoga for Flexbox)
react # Peer dependency
@inkjs/ui # Official component library: Select, TextInput, Spinner,
# ProgressBar, ConfirmInput, MultiSelect, Badge,
# StatusMessage, Alert, OrderedList, UnorderedList
figures # Unicode/ASCII symbol fallbacks (cross-platform)
Do NOT use the older standalone packages (ink-text-input, ink-select-input,
ink-spinner). The @inkjs/ui package supersedes them.
src/ui/tui/
├── App.tsx # Thin shell — calls screen registry factory
├── store.ts # WizardStore: nanostores + session setters
├── router.ts # WizardRouter: flow pipelines + overlay stack
├── ink-ui.ts # InkUI: bridges getUI() calls to store setters
├── start-tui.ts # TUI startup: dark mode, store, renderer
├── screen-registry.tsx # Maps screen names to components + services
├── styles.ts # Colors, Icons, alignment enums
├── screens/ # One file per screen — read for current set
├── primitives/ # Reusable layout components — read index.ts for exports
├── services/ # Injectable service interfaces
└── components/
└── TitleBar.tsx # Top bar with version + feedback email
Ink is react-dom but for terminals. It uses Yoga (Facebook's Flexbox engine) for layout.
Every <Box> is a flex container. All visible text MUST be inside <Text>.
| Browser | Ink |
|---|---|
<div> | <Box> |
<span> | <Text> |
| CSS / className | Props directly on <Box> and <Text> |
onClick | useInput() hook |
window.innerWidth | useStdout().stdout.columns |
| scroll | <Box overflow="hidden"> + manual offset |
display: block | <Box flexDirection="column"> |
display: flex | Default — every <Box> is already flex |
useStdout().stdout.columns and .rows!process.stdin.isTTY and fall back to LoggingUI--ci flag uses LoggingUI (no TUI, no prompts)start-tui.ts forces black background via ANSI escape codescolor="#000000" not color="black" (terminals render ANSI black as grey)useApp().exit()