ink-react-tui
Ink React TUI
Build interactive command-line interfaces using React components with Ink.
Quick Start
# Scaffold new project
npx create-ink-app my-cli
npx create-ink-app --typescript my-cli
# Or add to existing project
npm install ink react
Basic app:
import React from 'react';
import {render, Text} from 'ink';
const App = () => <Text color="green">Hello, CLI!</Text>;
render(<App />);
Core Components
Text
Display and style text. Only text nodes and nested <Text> allowed inside.
import {Text} from 'ink';
<Text color="green">Green text</Text>
<Text color="#005cc5">Hex color</Text>
<Text bold>Bold</Text>
<Text italic>Italic</Text>
<Text underline>Underlined</Text>
<Text strikethrough>Strikethrough</Text>
<Text inverse>Inverted colors</Text>
<Text dimColor>Dimmed</Text>
<Text backgroundColor="blue" color="white">With background</Text>
Text wrapping:
<Box width={10}>
<Text wrap="truncate">Long text gets truncated…</Text>
<Text wrap="truncate-middle">He…ld</Text>
<Text wrap="truncate-start">…World</Text>
</Box>
Box
Flexbox container (like <div style="display: flex">).
import {Box, Text} from 'ink';
// Basic layout
<Box flexDirection="column" padding={1}>
<Text>Row 1</Text>
<Text>Row 2</Text>
</Box>
// With border
<Box borderStyle="round" borderColor="green" padding={1}>
<Text>Bordered content</Text>
</Box>
// Flex alignment
<Box justifyContent="space-between" width={40}>
<Text>Left</Text>
<Text>Right</Text>
</Box>
Key properties:
- Dimensions:
width,height,minWidth,minHeight - Padding:
padding,paddingX,paddingY,paddingTop, etc. - Margin:
margin,marginX,marginY,marginTop, etc. - Flex:
flexDirection,flexGrow,flexShrink,flexWrap,alignItems,justifyContent - Borders:
borderStyle(single,double,round,bold,classic),borderColor - Background:
backgroundColor - Gap:
gap,columnGap,rowGap
For full property reference, see references/components.md.
Newline & Spacer
import {Text, Newline, Spacer, Box} from 'ink';
// Newline inside Text
<Text>
Line 1<Newline />Line 2<Newline count={2} />Line 4
</Text>
// Spacer fills available space
<Box width={40}>
<Text>Left</Text>
<Spacer />
<Text>Right</Text>
</Box>
Static
Render permanent output above the dynamic UI (logs, completed tasks).
import {Static, Box, Text} from 'ink';
const App = ({completedTasks}) => (
<>
<Static items={completedTasks}>
{task => (
<Box key={task.id}>
<Text color="green">✔ {task.title}</Text>
</Box>
)}
</Static>
<Box marginTop={1}>
<Text dimColor>Processing...</Text>
</Box>
</>
);
Transform
Transform rendered text output.
import {Transform, Text} from 'ink';
<Transform transform={output => output.toUpperCase()}>
<Text>hello world</Text>
</Transform>
// Output: HELLO WORLD
Essential Hooks
useInput
Handle keyboard input.
import {useInput, useApp} from 'ink';
const App = () => {
const {exit} = useApp();
useInput((input, key) => {
if (input === 'q') exit();
if (key.upArrow) { /* move up */ }
if (key.downArrow) { /* move down */ }
if (key.return) { /* select */ }
if (key.escape) { /* cancel */ }
});
return <Text>Press q to quit, arrows to navigate</Text>;
};
Key properties: leftArrow, rightArrow, upArrow, downArrow, return, escape, ctrl, shift, tab, backspace, delete, pageUp, pageDown, meta.
Disable input capture: useInput(handler, {isActive: false}).
useApp
Control app lifecycle.
import {useApp} from 'ink';
const App = () => {
const {exit} = useApp();
// Exit after 5 seconds
useEffect(() => {
setTimeout(() => exit(), 5000);
}, []);
return <Text>Exiting soon...</Text>;
};
useFocus & useFocusManager
Manage focus between components.
import {useFocus, Box, Text} from 'ink';
const FocusableItem = ({label}) => {
const {isFocused} = useFocus();
return (
<Box>
<Text color={isFocused ? 'green' : undefined}>
{isFocused ? '>' : ' '} {label}
</Text>
</Box>
);
};
// Tab cycles focus, Shift+Tab goes backwards
Focus options: autoFocus, isActive, id.
For complete hooks reference, see references/hooks.md.
Common Patterns
Interactive List
import React, {useState} from 'react';
import {render, Box, Text, useInput, useApp} from 'ink';
const items = ['Option A', 'Option B', 'Option C'];
const SelectList = () => {
const [selected, setSelected] = useState(0);
const {exit} = useApp();
useInput((input, key) => {
if (key.upArrow) setSelected(s => Math.max(0, s - 1));
if (key.downArrow) setSelected(s => Math.min(items.length - 1, s + 1));
if (key.return) {
console.log(`Selected: ${items[selected]}`);
exit();
}
if (input === 'q') exit();
});
return (
<Box flexDirection="column">
{items.map((item, i) => (
<Text key={item} color={i === selected ? 'green' : undefined}>
{i === selected ? '>' : ' '} {item}
</Text>
))}
</Box>
);
};
render(<SelectList />);
Progress Indicator
import React, {useState, useEffect} from 'react';
import {render, Box, Text} from 'ink';
const ProgressBar = ({percent}) => {
const width = 20;
const filled = Math.round(width * percent / 100);
return (
<Box>
<Text>[</Text>
<Text color="green">{'█'.repeat(filled)}</Text>
<Text>{'░'.repeat(width - filled)}</Text>
<Text>] {percent}%</Text>
</Box>
);
};
const App = () => {
const [progress, setProgress] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setProgress(p => p >= 100 ? 100 : p + 10);
}, 200);
return () => clearInterval(timer);
}, []);
return <ProgressBar percent={progress} />;
};
render(<App />);
Task List with Static Output
import React, {useState, useEffect} from 'react';
import {render, Static, Box, Text} from 'ink';
const App = () => {
const [completed, setCompleted] = useState([]);
const [current, setCurrent] = useState('Task 1');
useEffect(() => {
const tasks = ['Task 1', 'Task 2', 'Task 3'];
let i = 0;
const timer = setInterval(() => {
if (i < tasks.length) {
setCompleted(prev => [...prev, {id: i, title: tasks[i]}]);
i++;
setCurrent(tasks[i] || 'Done!');
}
}, 1000);
return () => clearInterval(timer);
}, []);
return (
<>
<Static items={completed}>
{task => (
<Text key={task.id} color="green">✔ {task.title}</Text>
)}
</Static>
<Text color="yellow">⏳ {current}</Text>
</>
);
};
render(<App />);
Render API
import {render} from 'ink';
const {rerender, unmount, waitUntilExit, clear} = render(<App />);
// Update props
rerender(<App count={2} />);
// Programmatic exit
unmount();
// Wait for exit
await waitUntilExit();
// Clear output
clear();
Options:
render(<App />, {
stdout: process.stdout,
stdin: process.stdin,
stderr: process.stderr,
exitOnCtrlC: true,
patchConsole: true,
debug: false,
maxFps: 30,
});
Testing
Use ink-testing-library:
import {render} from 'ink-testing-library';
import {Text} from 'ink';
const {lastFrame} = render(<Text>Hello</Text>);
expect(lastFrame()).toBe('Hello');
Useful Third-Party Components
| Package | Purpose |
|---|---|
ink-text-input |
Text input field |
ink-spinner |
Loading spinners |
ink-select-input |
Select/dropdown |
ink-progress-bar |
Progress bars |
ink-table |
Data tables |
ink-link |
Terminal hyperlinks |
ink-gradient |
Gradient text |
ink-big-text |
Large ASCII text |
For full list, see references/useful-components.md.
More from melvinmt/skills
agentic-website-design
Design and build websites using AI coding agents with static site generators. Covers Astro-first workflow, iterative visual refinement via browser feedback, skill-enhanced prompting (frontend-design, copywriting), animations, and high-bar polish loops. Use when building a website with an AI agent, designing landing pages, or iterating on web design with LLM assistance.
23machine-accessible-websites
Implement websites accessible to both humans and AI agents. Covers floating HUMAN/MACHINE toggle, llms.txt specification, content negotiation via Accept headers, .md alternate pages, and robots.txt exclusions. Use when building agent-friendly websites, implementing llms.txt, or adding machine-readable content.
2