Ink React CLI
Build rich interactive terminal UIs using Ink, a React renderer for the command line
You are an expert in building command-line interfaces with Ink, which brings React's component model to terminal applications. ## Key Points - Use `<Box>` with Flexbox properties (`flexDirection`, `gap`, `justifyContent`) for layout rather than manual spacing with spaces or newlines. - Leverage `useInput` for keyboard handling and always provide a way to quit (typically `q` or `Ctrl+C`). - Keep Ink components focused on display; put business logic in plain functions or hooks, not in render methods. - **Writing to `console.log` during Ink rendering** — stdout writes while Ink controls the terminal corrupt the display; use `<Text>` or the `<Static>` component for log-style output. - **Running expensive computation inside render** — Ink re-renders on every state change, just like React; performing CPU-intensive work in the render path blocks the terminal and drops keystrokes. - **Not providing a quit mechanism** — forgetting to handle `q` or `Ctrl+C` via `useInput` and `useApp().exit()` leaves users trapped in the application with no way to exit gracefully. - Writing to `console.log` while Ink is rendering corrupts the terminal output — use Ink's `<Text>` or the `<Static>` component for log-style output that persists above the interactive area. - Ink re-renders on every state change just like React, so avoid expensive computations in render — use `useMemo` and `useCallback` to prevent unnecessary work in fast-updating UIs. ## Quick Example ```bash npm install ink react npm install --save-dev @types/react ```
skilldb get cli-development-skills/Ink React CLIFull skill: 197 linesInk — CLI Development
You are an expert in building command-line interfaces with Ink, which brings React's component model to terminal applications.
Overview
Ink lets you build CLI output using React components rendered to the terminal. It supports Flexbox layout, hooks, state management, and the full React lifecycle — making it ideal for interactive tools, dashboards, and complex CLI UIs that go beyond simple text output.
Setup & Configuration
Install Ink and its peer dependencies:
npm install ink react
npm install --save-dev @types/react
Minimal Ink application:
import React from 'react';
import { render, Text, Box } from 'ink';
function App() {
return (
<Box flexDirection="column" padding={1}>
<Text bold color="green">Welcome to my CLI</Text>
<Text dimColor>Built with Ink</Text>
</Box>
);
}
render(<App />);
Configure tsconfig.json for JSX:
{
"compilerOptions": {
"jsx": "react-jsx",
"moduleResolution": "node",
"module": "ESNext",
"target": "ES2020"
}
}
Core Patterns
Interactive input with hooks
import React, { useState } from 'react';
import { render, Text, Box, useInput, useApp } from 'ink';
function Selector({ items }: { items: string[] }) {
const [selected, setSelected] = useState(0);
const { exit } = useApp();
useInput((input, key) => {
if (key.upArrow) setSelected(i => Math.max(0, i - 1));
if (key.downArrow) setSelected(i => Math.min(items.length - 1, i + 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 ? 'cyan' : undefined}>
{i === selected ? '❯ ' : ' '}{item}
</Text>
))}
</Box>
);
}
render(<Selector items={['Create', 'Read', 'Update', 'Delete']} />);
Async data loading with state
import React, { useState, useEffect } from 'react';
import { render, Text, Box } from 'ink';
import Spinner from 'ink-spinner';
function DataLoader({ url }: { url: string }) {
const [data, setData] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch(url)
.then(r => r.json())
.then(d => setData(JSON.stringify(d, null, 2)))
.catch(e => setError(e.message));
}, [url]);
if (error) return <Text color="red">Error: {error}</Text>;
if (!data) return <Text><Spinner type="dots" /> Loading...</Text>;
return <Text>{data}</Text>;
}
Table layout
import React from 'react';
import { Text, Box } from 'ink';
function Table({ rows }: { rows: { name: string; status: string; time: string }[] }) {
return (
<Box flexDirection="column">
<Box>
<Box width={20}><Text bold>Name</Text></Box>
<Box width={12}><Text bold>Status</Text></Box>
<Box width={10}><Text bold>Time</Text></Box>
</Box>
{rows.map(row => (
<Box key={row.name}>
<Box width={20}><Text>{row.name}</Text></Box>
<Box width={12}>
<Text color={row.status === 'pass' ? 'green' : 'red'}>{row.status}</Text>
</Box>
<Box width={10}><Text dimColor>{row.time}</Text></Box>
</Box>
))}
</Box>
);
}
Controlled exit and cleanup
import { render, useApp } from 'ink';
function App() {
const { exit } = useApp();
useEffect(() => {
doWork().then(() => exit()).catch(err => {
process.exitCode = 1;
exit(err);
});
}, []);
return <Text>Working...</Text>;
}
const instance = render(<App />);
instance.waitUntilExit().then(() => {
// cleanup after Ink unmounts
});
Best Practices
- Use
<Box>with Flexbox properties (flexDirection,gap,justifyContent) for layout rather than manual spacing with spaces or newlines. - Leverage
useInputfor keyboard handling and always provide a way to quit (typicallyqorCtrl+C). - Keep Ink components focused on display; put business logic in plain functions or hooks, not in render methods.
Core Philosophy
Ink brings React's component model to the terminal, and that means thinking in components, state, and effects rather than direct stdout manipulation. Just as React components describe what the UI should look like given current state, Ink components describe what the terminal should display. The framework handles diffing and re-rendering efficiently. Embrace this declarative model and avoid imperative terminal manipulation.
Keep display logic and business logic cleanly separated. Ink components should focus on rendering — layout, text formatting, user input handling. Heavy computation, file I/O, and network requests belong in custom hooks or plain functions that update state. This separation makes components testable and prevents re-render performance issues from blocking the event loop.
Terminal UIs have different constraints than web UIs. There is no CSS grid system, no browser DevTools, and no responsive breakpoints. Ink's Flexbox model covers the basics, but complex layouts require creative use of Box nesting and fixed widths. Embrace the constraint: terminal UIs should be information-dense and keyboard-driven, not visual replicas of web interfaces.
Anti-Patterns
-
Writing to
console.logduring Ink rendering — stdout writes while Ink controls the terminal corrupt the display; use<Text>or the<Static>component for log-style output. -
Running expensive computation inside render — Ink re-renders on every state change, just like React; performing CPU-intensive work in the render path blocks the terminal and drops keystrokes.
-
Not providing a quit mechanism — forgetting to handle
qorCtrl+CviauseInputanduseApp().exit()leaves users trapped in the application with no way to exit gracefully. -
Overcomplicating terminal layouts — building elaborate multi-panel dashboards with deeply nested Flex containers pushes Ink beyond its strengths; keep layouts simple and use fixed widths for predictable rendering.
-
Mixing Ink with raw terminal libraries — combining Ink with libraries like
blessedor direct ANSI escape sequences creates rendering conflicts where both systems fight over cursor position and screen clearing.
Common Pitfalls
- Writing to
console.logwhile Ink is rendering corrupts the terminal output — use Ink's<Text>or the<Static>component for log-style output that persists above the interactive area. - Ink re-renders on every state change just like React, so avoid expensive computations in render — use
useMemoanduseCallbackto prevent unnecessary work in fast-updating UIs.
Install this skill directly: skilldb add cli-development-skills
Related Skills
Chalk Picocolors
Style terminal output with chalk and picocolors for colored, formatted CLI text
Clack Prompts
Build beautiful interactive CLI prompts using @clack/prompts with minimal boilerplate
CLI Distribution
Package and distribute CLI tools via npm/npx, standalone binaries with pkg, and Homebrew taps
CLI Testing
Test CLI applications using subprocess execution, mock filesystems, and snapshot testing
Commander Js
Build structured CLI applications with Commander.js including commands, options, and argument parsing
Oclif Framework
Build production-grade, extensible CLI tools using the oclif framework with TypeScript