| name | ink-cli |
| description | Build terminal CLI apps using Ink (React for CLIs). This skill should be used when the user wants to create command-line interfaces with React components, terminal UIs, interactive CLI tools, or needs help with Ink components, hooks, and patterns. |
Ink CLI Development
Ink is a React renderer for building command-line interfaces. It uses Yoga for Flexbox layouts and provides the same component-based experience as React in the browser.
Quick Start
# Scaffold new project
npx create-ink-app my-cli
# With TypeScript (recommended)
npx create-ink-app --typescript my-cli
Core Concepts
Every Element is a Flexbox Container
Think of <Box> as <div style="display: flex">. All text must be wrapped in <Text>.
Component Hierarchy
import {render, Box, Text} from 'ink';
const App = () => (
<Box flexDirection="column" padding={1}>
<Text bold>Title</Text>
<Box gap={2}>
<Text color="green">Left</Text>
<Text color="blue">Right</Text>
</Box>
</Box>
);
render(<App />);
Essential Components
<Text> - Styled Text
<Text color="green">Green text</Text>
<Text color="#005cc5">Hex color</Text>
<Text bold italic underline>Styled</Text>
<Text dimColor>Dimmed</Text>
<Text inverse>Inverted</Text>
<Text wrap="truncate">Long text will be truncated...</Text>
<Box> - Flexbox Container
// Layout
<Box flexDirection="column" alignItems="center" justifyContent="space-between">
// Spacing
<Box padding={2} margin={1} gap={1}>
// Dimensions
<Box width={50} height={10} minWidth={20}>
<Box width="50%"> {/* Percentage of parent */}
// Borders
<Box borderStyle="round" borderColor="green">
<Box borderStyle="double" borderTop borderBottom>
// Background
<Box backgroundColor="blue">
Border styles: single, double, round, bold, singleDouble, doubleSingle, classic
<Static> - Permanent Output
Renders above everything else, never re-renders. Use for logs, completed items.
const App = () => {
const [logs, setLogs] = useState<string[]>([]);
return (
<>
<Static items={logs}>
{(log, i) => <Text key={i}>{log}</Text>}
</Static>
<Box>
<Text>Live UI here</Text>
</Box>
</>
);
};
<Newline> and <Spacer>
<Text>Line 1<Newline />Line 2</Text>
<Newline count={3} />
<Box>
<Text>Left</Text>
<Spacer /> {/* Pushes Right to the edge */}
<Text>Right</Text>
</Box>
<Transform> - String Transformation
<Transform transform={output => output.toUpperCase()}>
<Text>hello</Text> {/* Renders: HELLO */}
</Transform>
Essential Hooks
useInput - Keyboard Input
import {useInput, useApp} from 'ink';
const App = () => {
const {exit} = useApp();
useInput((input, key) => {
if (input === 'q') exit();
if (key.leftArrow) { /* handle left */ }
if (key.return) { /* handle enter */ }
if (key.escape) { /* handle escape */ }
});
return <Text>Press q to quit</Text>;
};
Key properties: leftArrow, rightArrow, upArrow, downArrow, return, escape, ctrl, shift, tab, backspace, delete, pageUp, pageDown, meta
useApp - App Control
const {exit} = useApp();
exit(); // Clean exit
exit(error); // Exit with error
useFocus - Focus Management
const Item = ({label}) => {
const {isFocused} = useFocus();
return (
<Text color={isFocused ? 'green' : 'white'}>
{isFocused ? '>' : ' '} {label}
</Text>
);
};
Options: autoFocus, isActive, id
useFocusManager - Programmatic Focus
const {focusNext, focusPrevious, focus} = useFocusManager();
focus('specific-id');
useStdout / useStderr - Direct Output
const {write} = useStdout();
write('Direct to stdout\n'); // Bypasses Ink rendering
Common Patterns
Loading Spinner
import Spinner from 'ink-spinner';
const Loading = ({text}) => (
<Text>
<Text color="green"><Spinner type="dots" /></Text>
{' '}{text}
</Text>
);
Progress Indicator
const Progress = ({percent}) => {
const width = 20;
const filled = Math.round(width * percent / 100);
return (
<Box>
<Text color="green">{'█'.repeat(filled)}</Text>
<Text color="gray">{'░'.repeat(width - filled)}</Text>
<Text> {percent}%</Text>
</Box>
);
};
Selectable List
const List = ({items}) => {
const [selected, setSelected] = useState(0);
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));
});
return (
<Box flexDirection="column">
{items.map((item, i) => (
<Text key={i} color={i === selected ? 'green' : 'white'}>
{i === selected ? '>' : ' '} {item}
</Text>
))}
</Box>
);
};
Table Layout
const Table = ({data}) => (
<Box flexDirection="column">
{data.map((row, i) => (
<Box key={i} gap={2}>
{row.map((cell, j) => (
<Box key={j} width={15}>
<Text>{cell}</Text>
</Box>
))}
</Box>
))}
</Box>
);
Testing
import {render} from 'ink-testing-library';
const {lastFrame, rerender} = render(<MyComponent />);
expect(lastFrame()).toContain('expected text');
API Reference
For complete API documentation, see references/ink-api.md.
For community components (spinners, inputs, tables, etc.), see references/components.md.
Tips
- All text in
<Text>: Never put raw text directly in<Box> - Use
<Static>for logs: Prevents re-rendering of completed output - Disable input when needed:
useInput(handler, {isActive: false}) - Debug with React DevTools: Run with
DEV=true my-cli - Handle Ctrl+C: Enabled by default, disable with
exitOnCtrlC: falsein render options