TermUI is a React-based terminal UI framework that lets developers build rich, interactive CLI applications using the same mental model as web UI development. It ships as a shadcn/ui-style component library — instead of installing a runtime package, you copy components directly into your project via a CLI (npx termui add spinner), giving you full ownership and customization.
Core value propositions:
- No runtime lock-in — components live in your codebase
- First-class TypeScript support
- Built-in theming, accessibility, and AI-native tooling
- Works with any Node.js CLI framework via adapters
Built on pnpm workspaces + Turborepo for parallel builds and caching.
packages/
core/ — Hooks, theme engine, terminal primitives (@termui/core)
components/ — 101 UI components in 13 categories (@termui/components)
cli/ — The npx termui CLI tool (termui)
testing/ — Headless test utilities (@termui/testing)
adapters/ — 28+ wrappers for chalk, ora, inquirer, AI SDKs, etc.
types/ — Framework-agnostic TypeScript types (@termui/types)
registry/ — Component manifest JSON (schema + metadata)
templates/ — Starter project templates
examples/ — Demo apps
| Layer | Technology | Why |
|---|---|---|
| Language | TypeScript (ESM-only) | Type safety, tree-shaking |
| Runtime | Node.js 18+ | Native fetch, ESM support |
| Renderer | Ink 5 | React-to-terminal renderer |
| Layout engine | Yoga (Facebook) | Flexbox in the terminal |
| Build | tsup / tsc | Fast ESM + CJS dual output |
| Tests | Vitest | Fast, ESM-native |
| Package manager | pnpm 9 | Workspace deduplication |
| Task runner | Turborepo | Parallel builds, cache |
Ink is the critical piece — it runs a React reconciler that renders JSX as terminal output using Yoga's flexbox layout engine. Everything in TermUI builds on top of it.
This is the engine room. It exports three subpaths:
@termui/core— hooks + theme utilities@termui/core/styling— ThemeProvider@termui/core/hooks— all interaction hooks
| Hook | What it does |
|---|---|
useInput |
Raw keyboard input from Ink |
useFocus / useFocusManager |
Ink focus state + programmatic control |
useKeyboardNavigation |
List navigation — arrows, Home/End, PgUp/Down, Enter, Escape |
useFocusTrap |
Traps Tab focus within overlays (Modal, Dialog, Drawer) |
useTheme |
Reads current theme tokens from context |
useTerminal |
Terminal size (cols, rows, color depth) |
useAnimation |
Frame-based animation with FPS control |
useInterval |
Safe setInterval that cleans up on unmount |
useClipboard |
Read/write clipboard via OSC 52 escape sequences |
useKeymap |
Declarative keybinding mapping |
useMouse |
Mouse event handling |
useResize |
Detect terminal resize events |
useAsync |
Async data loading with loading/error/data state |
useMotion |
Reads NO_MOTION / CI env vars for animation opt-out |
useUnicode |
Reads NO_UNICODE env var for ASCII fallbacks |
useNotifications |
Toast/notification queue |
usePushToTalk |
Voice dictation input capture |
useKeyboardNavigation internals:
Wraps useInput to dispatch arrow keys, Home/End, Page Up/Down to an index state. It supports both looping (wraps from last → first) and clamping. The Enter/Space callbacks call back into the component's selection logic. Every interactive list (Select, Table, Tree) delegates to this one hook.
useFocusTrap internals:
Takes an array of focusable IDs. On Tab it advances the active focus index within that array using Ink's focusManager. On Shift+Tab it reverses. When the trap is active (e.g., a modal opens), focus is contained to the provided IDs and cannot escape to background elements. This is the accessibility story for overlays.
101 components across 13 categories. Each is a React functional component that:
- Accepts typed props (with sensible defaults)
- Calls
useTheme()to read tokens - Calls
useMotion()/useUnicode()for environment adaptation - Renders Ink primitives (
<Text>,<Box>) with Yoga flexbox
Pattern example (Spinner):
export function Spinner({ style = 'dots', label, fps = 10 }: SpinnerProps) {
const theme = useTheme();
const { reduced } = useMotion();
const unicode = useUnicode();
const frame = useAnimation(fps);
if (reduced) return <Text>{label ?? '...'}</Text>;
const frames = unicode ? FRAMES[style] : ASCII_FRAMES[style];
return (
<Text color={theme.colors.primary}>
{frames[frame % frames.length]} {label}
</Text>
);
}AI components (in /ai/ category):
ChatMessage,ChatThread— Render conversationsToolCall,ToolApproval— Show/confirm function callsThinkingBlock— Collapsible reasoning outputContextMeter— Context window progress with warning zonesTokenUsage— Prompt/completion/cost displayModelSelector— Interactive model picker grouped by provider
Architecture: React context (ThemeProvider) → useTheme() hook in every component.
<ThemeProvider theme={draculaTheme}>
<App />
</ThemeProvider>ThemeProvider reads COLORFGBG env var to auto-detect dark/light mode. AutoThemeProvider is a wrapper that switches between two themes automatically.
10 built-in themes: default, dracula, nord, catppuccin, monokai, tokyo-night, one-dark, solarized, high-contrast, high-contrast-light.
Token shape:
interface Theme {
colors: {
primary, secondary, accent,
background, surface, border,
text, textMuted, textInverse,
success, warning, error, info,
focusRing, selection,
// 20+ semantic colors total
};
spacing: { xs, sm, md, lg, xl };
radius: { sm, md, lg };
}Custom theme:
const myTheme = createTheme('brand', {
colors: { primary: '#FF6B6B', focusRing: '#FF6B6B' }
});The CLI is the distribution mechanism. It's dogfooded — built with @termui/adapters/args (TermUI's own CLI builder adapter).
| Command | What it does |
|---|---|
init |
Creates termui.config.json in the project |
add <component> |
Fetch + write component from registry |
list |
Browse all components by category |
diff <component> |
Show local vs. upstream registry diff |
update <component> |
Re-download from registry |
theme [name] |
List or apply a theme |
preview |
Interactive component gallery (Ink TUI) |
mcp |
Start the MCP server on stdio |
create <name> |
Scaffold a new project from a template |
publish |
Submit to community registry |
File: termui.config.json
{
"version": "1",
"componentsDir": "./components/ui",
"registry": "https://arindam200.github.io/termui",
"registries": [],
"theme": "default"
}Search order: .json → .ts → .js. The .json file is the only one the CLI writes to; the .ts/.js forms are for TypeScript-first projects.
- Read
termui.config.jsonto find registry URL(s) GET /registry/schema.json→ parse manifest- Resolve
ComponentMetafor each requested component - Recursively resolve
peerComponents(dependencies) GETeach source file from registry- Apply Prettier formatting using the user's local config (if present)
- Write to
{componentsDir}/{category}/{ComponentName}.tsx - Print peer npm dependencies the user needs to install manually
Fuzzy matching: If the component name doesn't match exactly, Levenshtein distance is used to suggest the closest match.
Dry-run: --dry-run flag shows what would be written without touching disk.
Recipes: --recipe login-flow installs a pre-configured set of components and writes wiring code in one step.
Multi-registry: The registries array in config allows community registries to override core components. Resolution is last-wins — components from later registries shadow earlier ones.
The registry is a static JSON file hosted on GitHub Pages with a jsDelivr CDN fallback. The CLI binary also embeds a minimal offline fallback manifest.
Fallback chain:
https://arindam200.github.io/termui/registry/schema.jsonhttps://cdn.jsdelivr.net/gh/arindam200/termui@main/registry/schema.json- Embedded local manifest (always-available offline mode)
ComponentMeta shape:
interface ComponentMeta {
name: string;
description: string;
version: string;
category: string;
deps?: string[]; // npm packages to install
peerComponents?: string[]; // auto-installed TermUI components
files: string[]; // file paths in registry
author?: { name, url, github };
registry?: 'core' | 'community';
}The termui mcp command starts a Model Context Protocol server over stdio so AI assistants (Claude Code, Cursor, GitHub Copilot) can browse and install components natively.
5 tools exposed:
| Tool | Parameters | Description |
|---|---|---|
list_components |
category?, filter? |
Browse all components grouped by category |
search_components |
query |
Ranked keyword search |
get_component_docs |
name |
Full props, usage, source files |
add_component |
names[], dryRun? |
Install into current project |
get_theme_tokens |
theme? |
List themes and token structure |
Search ranking algorithm (search_components):
+20 exact name match
+10 name contains query
+5 description contains query
+3 category match
+4 per-word name match
+2 per-word description match
+3 per-word category exact match
Returns top 10 results.
MCP installation (npx termui add mcp):
Prompts for scope, then writes the server config to:
.mcp.json(current project / Claude Code)~/.claude/settings.json(global Claude Code)~/Library/Application Support/Claude/claude_desktop_config.json(Claude Desktop)
Ink renders to a string, not a DOM — so TermUI has its own test utilities built on top of that.
| Export | Purpose |
|---|---|
renderToString(component) |
Single-frame string render |
createTestRenderer() |
Reusable renderer with cleanup() |
screen.getByText() / hasText() |
Query the rendered output |
fireEvent.key() / type() / press() |
Simulate keyboard input |
waitFor(fn, opts) |
Poll an assertion until it passes or times out |
stripVolatile(str) |
Remove spinner frames and timestamps for stable snapshots |
normalizeAnsi(str) |
Normalize ANSI escape codes for snapshot comparison |
testCLI() |
Full CLI test harness with mock registry + filesystem |
28+ drop-in replacements for popular CLI libraries that integrate TermUI theming. The point is to let existing codebases adopt TermUI theming without rewriting everything.
Key adapters:
chalk— chalk-compatible coloring API reading theme tokensora— ora-compatible spinner backed bySpinnercomponentclack— @clack/prompts-style interactive promptsinquirer— inquirer-compatible promptscommander/meow— argument parsing with TermUI help formattingai— hooks for Anthropic, OpenAI, Ollama (useChat,useCompletion,useStream)voice— FFmpeg-based voice capture for push-to-talk dictationpty— Pseudo-terminal controlgit/github— Git operations and GitHub API wrapper
1. Copy-paste distribution (shadcn model)
Components are copied into your repo, not imported from npm.
- Pro: No version lock-in, full customization, zero runtime dependency on TermUI infra.
- Con: Updates require re-running
npx termui update; no automatic patch delivery.
2. Ink as the renderer (not raw ANSI strings)
TermUI is a React library rendering through Ink's reconciler to the terminal.
- Pro: React component model, diffing/reconciliation, hooks, JSX — familiar to web devs.
- Con: Ink adds overhead; not the right tool for simple one-liner CLIs.
3. Hook-based architecture for all interactivity
Every interactive behavior (navigation, focus, animation) is a hook.
- Pro: Composable, testable, shareable across components. No hidden global state.
- Con: Requires React knowledge; not usable outside React component trees.
4. Theme via React context, not CSS variables
useTheme()reads from a context provider; no stylesheet system exists.
- Pro: Works in a string-rendering environment with no DOM.
- Con: Can't do media queries or cascade; all theming is explicit and programmatic.
5. Static registry on GitHub Pages
No server, no auth, no database. Just a JSON file on a CDN.
- Pro: Zero infra cost, high availability, community-forkable.
- Con: No search-as-a-service, no versioned component history (mitigated by
diff/update).
6. MCP server on stdio transport
No HTTP listener, no port — communicates via process stdin/stdout.
- Pro: Zero network config, works inside any AI tool that supports MCP.
- Con: One client at a time; can't serve multiple AI agents simultaneously.
7. Accessibility via env vars
NO_UNICODE=1,NO_MOTION=1,CI=truetrigger safe fallbacks automatically.
- Pro: Works without any config in CI pipelines; CI gets stable, animation-free output.
- Con: Coarse-grained; you can't selectively disable motion for one component at runtime.
"Walk me through how npx termui add button works"
Read config → fetch registry manifest → resolve component + peer components recursively → fetch source files → format with Prettier → write to disk → print required npm dependencies.
"How is the theming system implemented?"
React context with a typed token object. ThemeProvider wraps the app. Every component calls useTheme() to get colors/spacing/radius. Auto dark/light detection via COLORFGBG env var. Custom themes via createTheme().
"How does keyboard accessibility work?"
useKeyboardNavigation gives every list a consistent UX (arrows, Home/End, PgUp/Down). useFocusTrap contains focus within modals/dialogs during overlay lifecycles, cycling through focusable IDs with Tab/Shift+Tab.
"Why shadcn model and not a traditional npm package?" Ownership, customization, no runtime dependency on TermUI infra. Components are just TypeScript files in the user's repo — they can change anything without waiting for upstream.
"How does the MCP integration work?"
termui mcp starts a stdio MCP server exposing 5 tools. AI assistants call these tools to search for components, read their docs, and install them directly into the project. No HTTP, no auth — pure stdio JSON-RPC.
"How do you test terminal UI components?"
Ink renders to a string, not a DOM. @termui/testing provides renderToString(), screen query helpers, keyboard event simulation, waitFor() for async state, and stripVolatile() to remove animation frames before snapshotting.