Overview
React Ink brings React's component model to the terminal. Instead of rendering to the DOM, Ink renders to stdout using a custom React reconciler backed by the Yoga layout engine (the same Flexbox implementation used by React Native). Build interactive CLI tools with components like <Box> for layout and <Text> for styled output, handle keyboard input with useInput, and manage focus with useFocus - all using familiar React patterns including hooks, state, effects, Suspense, and concurrent rendering.
When to Use
Trigger this skill when the user:
- Wants to build an interactive CLI application using React
- Needs terminal UI components with Flexbox layout (Box, Text)
- Is handling keyboard input in a terminal app with
useInput
- Wants focus management across terminal UI elements
- Needs to display progress, spinners, or streaming logs in a CLI
- Is scaffolding a new CLI project with
create-ink-app
- Wants to render styled text with colors, borders, or formatting in the terminal
Do NOT trigger this skill for:
- General React web or React Native development (use frontend-developer)
- Simple shell scripts that just print output (use shell-scripting)
Prerequisites
- Node >= 20
- React >= 19
- Ink v6+ is ESM-only (
"type": "module" in package.json)
- Windows host is primary (PowerShell). Commands provided are compatible with PowerShell unless noted.
Procedure
Install Ink and React
npm install ink react
Or scaffold a full project:
npx create-ink-app my-cli
npx create-ink-app my-cli --typescript
Create a basic app
import React, {useState, useEffect} from 'react';
import {render, Text} from 'ink';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1);
}, 100);
return () => clearInterval(timer);
}, []);
return <Text color="green">{count} tests passed</Text>;
}
render(<Counter />);
Render an app and handle exit
import {render, useApp, useInput, Text} from 'ink';
function App() {
const {exit} = useApp();
useInput((input, key) => {
if (input === 'q') exit();
});
return <Text>Press q to quit</Text>;
}
const instance = render(<App />);
await instance.waitUntilExit();
console.log('Goodbye!');
Build a layout with Box
import {Box, Text} from 'ink';
function Dashboard() {
return (
<Box flexDirection="column" padding={1}>
<Box borderStyle="round" borderColor="blue" paddingX={1}>
<Text bold>Header</Text>
</Box>
<Box gap={2}>
<Box flexDirection="column" width="50%">
<Text color="green">Left panel</Text>
</Box>
<Box flexDirection="column" width="50%">
<Text color="yellow">Right panel</Text>
</Box>
</Box>
</Box>
);
}
Handle keyboard input
import {useState} from 'react';
import {useInput, Text, Box} from 'ink';
function Movement() {
const [x, setX] = useState(0);
const [y, setY] = useState(0);
useInput((_input, key) => {
if (key.leftArrow) setX(prev => Math.max(0, prev - 1));
if (key.rightArrow) setX(prev => Math.min(20, prev + 1));
if (key.upArrow) setY(prev => Math.max(0, prev - 1));
if (key.downArrow) setY(prev => Math.min(10, prev + 1));
});
return (
<Box flexDirection="column">
<Text>Position: {x}, {y}</Text>
<Text>Use arrow keys to move</Text>
</Box>
);
}
Build a focusable selection list
import {Box, Text, useFocus} from 'ink';
function Item({label}: {label: string}) {
const {isFocused} = useFocus();
return (
<Text color={isFocused ? 'blue' : undefined}>
{isFocused ? '>' : ' '} {label}
</Text>
);
}
function SelectList() {
return (
<Box flexDirection="column">
<Item label="Option A" />
<Item label="Option B" />
<Item label="Option C" />
</Box>
);
}
Tab and Shift+Tab cycle focus. Use useFocusManager().focus(id) for programmatic control.
Display streaming logs with Static
import {useState, useEffect} from 'react';
import {render, Static, Box, Text} from 'ink';
function BuildOutput() {
const [logs, setLogs] = useState<string[]>([]);
const [current, setCurrent] = useState('Starting...');
useEffect(() => {
const timer = setInterval(() => {
setLogs(prev => [...prev, current]);
setCurrent(`Building step ${prev.length + 1}...`);
}, 500);
return () => clearInterval(timer);
}, []);
return (
<Box flexDirection="column">
<Static items={logs}>
{(log, i) => <Text key={i} color="green"> {log}</Text>}
</Static>
<Text color="yellow"> {current}</Text>
</Box>
);
}
Use Suspense for async data
import React, {Suspense} from 'react';
import {render, Text} from 'ink';
let data: string | undefined;
let promise: Promise<void> | undefined;
function fetchData() {
if (data) return data;
if (!promise) {
promise = new Promise(resolve => {
setTimeout(() => { data = 'Loaded!'; resolve(); }, 1000);
});
}
throw promise;
}
function DataView() {
const result = fetchData();
return <Text color="green">{result}</Text>;
}
render(
<Suspense fallback={<Text color="yellow">Loading...</Text>}>
<DataView />
</Suspense>
);
Respond to terminal resize
import {useWindowSize, Box, Text} from 'ink';
function ResponsiveLayout() {
const {columns, rows} = useWindowSize();
return (
<Box flexDirection="column">
<Text>Terminal: {columns}x{rows}</Text>
<Box width={columns > 80 ? '50%' : '100%'}>
<Text>Content adapts to terminal size</Text>
</Box>
</Box>
);
}
Pitfalls
Raw text inside <Box> silently breaks rendering - Placing a string directly inside <Box> without wrapping it in <Text> causes a runtime error. Unlike web React where a <div> can contain bare text, Ink enforces that only <Text> components hold text content. Always wrap strings in <Text>.
useInput does nothing without raw mode on stdin - If stdin is not in raw mode (e.g., piped input in CI, non-TTY environments), useInput never fires. Check useStdin().isRawModeSupported before relying on keyboard input, and provide a non-interactive fallback for CI/piped contexts.
Ink v6 is ESM-only and breaks CommonJS imports - Importing Ink with require('ink') throws require() of ES Module. You must use import syntax and set "type": "module" in your package.json. This also means Ink v6 cannot be used in projects that are stuck on CommonJS without a build step.
<Static> items must have stable keys or they re-render - The <Static> component renders each item exactly once and never updates it. If you pass items without stable key props or if you mutate the items array in place instead of appending, previously rendered lines can disappear or duplicate.
The app stays alive as long as stdin listeners or timers exist - Ink's render() keeps the process running while there are pending timers, promises, or stdin listeners. Forgetting to call clearInterval, clearTimeout, or exit() from useApp() results in a CLI tool that hangs after the work is done.
stdin.setRawMode is not a function - Running in non-TTY environment (piped input, CI). Check isRawModeSupported from useStdin() before enabling.
React is not defined - Missing React import with JSX transform. Add import React from 'react' or configure JSX automatic runtime.
Verification
- Check Node version:
node -v (must be >= 20)
- Check package.json for
"type": "module" if using Ink v6+
- Run the CLI app:
node dist/index.js or npm start
- Verify interactive input works in a real terminal (not piped)
- Verify the process exits cleanly after completion (no hanging)
- Verify no raw text is placed directly inside
<Box> components
UI/UX 2026 Guidelines
This skill also covers interface design, UX review, accessibility, responsive layouts, design systems, mobile/web UI, component behavior, interaction states, visual hierarchy, usability improvements, and frontend implementation guidance.
Workflow:
- Start from the user task and information architecture, not decoration.
- Map key states: empty, loading, success, error, disabled, permission-limited, offline, and responsive variants.
- Apply accessibility requirements early: keyboard flow, focus visibility, labels, contrast, reduced motion, touch targets, text resizing, and semantic structure.
- Use design-system primitives where available; otherwise define tokens for spacing, color, type, elevation, radius, and motion.
- Design responsive layouts with stable dimensions and no text overlap across desktop and mobile.
- Validate with realistic content, long labels, error text, and touch/keyboard interaction.
- Deliver concrete implementation guidance, not vague aesthetic notes.
Quality Checklist:
- User can complete the core task quickly and repeatedly.
- UI supports keyboard, screen readers, visible focus, and sufficient contrast.
- Mobile and desktop layouts do not overlap or rely on fragile viewport-scaled text.
- Controls use familiar affordances and expose state clearly.
- Motion is purposeful and respects reduced-motion preferences.
- Visual direction is intentional and consistent with the product domain.
Failure Handling:
If requirements conflict, prioritize usability, accessibility, and product fit over novelty. If a requested visual pattern harms readability or accessibility, explain the tradeoff and offer a better variant. Verify current platform guidance when building for Apple, Android, or a specific design system.
Current References:
References
This skill does not ship a companion pack. Box/Text/hooks usage is in Procedure above. Full props, community components, and example apps:
Related skills
- frontend-developer: For general React web or React Native development.
- shell-scripting: For simple shell scripts that just print output.
1---2name: react-ink3description: Builds interactive terminal UIs with React Ink: Box and Text, Yoga flexbox, useInput, useFocus, Static logs, and create-ink-app. Use when the user wants a React CLI, Ink components, or styled stdout UI. Not for React DOM, Next.js, or React Native screens. Do not use for shell scripts that only print text.4---5
6## Overview
7
8React Ink brings React's component model to the terminal. Instead of rendering to the DOM, Ink renders to stdout using a custom React reconciler backed by the Yoga layout engine (the same Flexbox implementation used by React Native). Build interactive CLI tools with components like `<Box>` for layout and `<Text>` for styled output, handle keyboard input with `useInput`, and manage focus with `useFocus` - all using familiar React patterns including hooks, state, effects, Suspense, and concurrent rendering.
9
10## When to Use
11
12Trigger this skill when the user:
13- Wants to build an interactive CLI application using React
14- Needs terminal UI components with Flexbox layout (Box, Text)
15- Is handling keyboard input in a terminal app with `useInput`
16- Wants focus management across terminal UI elements
17- Needs to display progress, spinners, or streaming logs in a CLI
18- Is scaffolding a new CLI project with `create-ink-app`
19- Wants to render styled text with colors, borders, or formatting in the terminal
20
21Do NOT trigger this skill for:
22- General React web or React Native development (use frontend-developer)
23- Simple shell scripts that just print output (use shell-scripting)
24
25## Prerequisites
26
27- Node >= 20
28- React >= 19
29- Ink v6+ is ESM-only (`"type": "module"` in package.json)
30- Windows host is primary (PowerShell). Commands provided are compatible with PowerShell unless noted.
31
32## Procedure
33
341. **Install Ink and React**
35 ```bash
36 npm install ink react
37 ```
38 Or scaffold a full project:
39 ```bash
40 npx create-ink-app my-cli
41 npx create-ink-app my-cli --typescript
42 ```
43
442. **Create a basic app**
45 ```tsx
46 import React, {useState, useEffect} from 'react';
47 import {render, Text} from 'ink';
48
49 function Counter() {
50 const [count, setCount] = useState(0);
51
52 useEffect(() => {
53 const timer = setInterval(() => {
54 setCount(prev => prev + 1);
55 }, 100);
56 return () => clearInterval(timer);
57 }, []);
58
59 return <Text color="green">{count} tests passed</Text>;
60 }
61
62 render(<Counter />);
63 ```
64
653. **Render an app and handle exit**
66 ```tsx
67 import {render, useApp, useInput, Text} from 'ink';
68
69 function App() {
70 const {exit} = useApp();
71 useInput((input, key) => {
72 if (input === 'q') exit();
73 });
74 return <Text>Press q to quit</Text>;
75 }
76
77 const instance = render(<App />);
78 await instance.waitUntilExit();
79 console.log('Goodbye!');
80 ```
81
824. **Build a layout with Box**
83 ```tsx
84 import {Box, Text} from 'ink';
85
86 function Dashboard() {
87 return (
88 <Box flexDirection="column" padding={1}>
89 <Box borderStyle="round" borderColor="blue" paddingX={1}>
90 <Text bold>Header</Text>
91 </Box>
92 <Box gap={2}>
93 <Box flexDirection="column" width="50%">
94 <Text color="green">Left panel</Text>
95 </Box>
96 <Box flexDirection="column" width="50%">
97 <Text color="yellow">Right panel</Text>
98 </Box>
99 </Box>
100 </Box>
101 );
102 }
103 ```
104
1055. **Handle keyboard input**
106 ```tsx
107 import {useState} from 'react';
108 import {useInput, Text, Box} from 'ink';
109
110 function Movement() {
111 const [x, setX] = useState(0);
112 const [y, setY] = useState(0);
113
114 useInput((_input, key) => {
115 if (key.leftArrow) setX(prev => Math.max(0, prev - 1));
116 if (key.rightArrow) setX(prev => Math.min(20, prev + 1));
117 if (key.upArrow) setY(prev => Math.max(0, prev - 1));
118 if (key.downArrow) setY(prev => Math.min(10, prev + 1));
119 });
120
121 return (
122 <Box flexDirection="column">
123 <Text>Position: {x}, {y}</Text>
124 <Text>Use arrow keys to move</Text>
125 </Box>
126 );
127 }
128 ```
129
1306. **Build a focusable selection list**
131 ```tsx
132 import {Box, Text, useFocus} from 'ink';
133
134 function Item({label}: {label: string}) {
135 const {isFocused} = useFocus();
136 return (
137 <Text color={isFocused ? 'blue' : undefined}>
138 {isFocused ? '>' : ' '} {label}
139 </Text>
140 );
141 }
142
143 function SelectList() {
144 return (
145 <Box flexDirection="column">
146 <Item label="Option A" />
147 <Item label="Option B" />
148 <Item label="Option C" />
149 </Box>
150 );
151 }
152 ```
153 Tab and Shift+Tab cycle focus. Use `useFocusManager().focus(id)` for programmatic control.
154
1557. **Display streaming logs with Static**
156 ```tsx
157 import {useState, useEffect} from 'react';
158 import {render, Static, Box, Text} from 'ink';
159
160 function BuildOutput() {
161 const [logs, setLogs] = useState<string[]>([]);
162 const [current, setCurrent] = useState('Starting...');
163
164 useEffect(() => {
165 const timer = setInterval(() => {
166 setLogs(prev => [...prev, current]);
167 setCurrent(`Building step ${prev.length + 1}...`);
168 }, 500);
169 return () => clearInterval(timer);
170 }, []);
171
172 return (
173 <Box flexDirection="column">
174 <Static items={logs}>
175 {(log, i) => <Text key={i} color="green"> {log}</Text>}
176 </Static>
177 <Text color="yellow"> {current}</Text>
178 </Box>
179 );
180 }
181 ```
182
1838. **Use Suspense for async data**
184 ```tsx
185 import React, {Suspense} from 'react';
186 import {render, Text} from 'ink';
187
188 let data: string | undefined;
189 let promise: Promise<void> | undefined;
190
191 function fetchData() {
192 if (data) return data;
193 if (!promise) {
194 promise = new Promise(resolve => {
195 setTimeout(() => { data = 'Loaded!'; resolve(); }, 1000);
196 });
197 }
198 throw promise;
199 }
200
201 function DataView() {
202 const result = fetchData();
203 return <Text color="green">{result}</Text>;
204 }
205
206 render(
207 <Suspense fallback={<Text color="yellow">Loading...</Text>}>
208 <DataView />
209 </Suspense>
210 );
211 ```
212
2139. **Respond to terminal resize**
214 ```tsx
215 import {useWindowSize, Box, Text} from 'ink';
216
217 function ResponsiveLayout() {
218 const {columns, rows} = useWindowSize();
219 return (
220 <Box flexDirection="column">
221 <Text>Terminal: {columns}x{rows}</Text>
222 <Box width={columns > 80 ? '50%' : '100%'}>
223 <Text>Content adapts to terminal size</Text>
224 </Box>
225 </Box>
226 );
227 }
228 ```
229
230## Pitfalls
231
2321. **Raw text inside `<Box>` silently breaks rendering** - Placing a string directly inside `<Box>` without wrapping it in `<Text>` causes a runtime error. Unlike web React where a `<div>` can contain bare text, Ink enforces that only `<Text>` components hold text content. Always wrap strings in `<Text>`.
233
2342. **`useInput` does nothing without raw mode on stdin** - If stdin is not in raw mode (e.g., piped input in CI, non-TTY environments), `useInput` never fires. Check `useStdin().isRawModeSupported` before relying on keyboard input, and provide a non-interactive fallback for CI/piped contexts.
235
2363. **Ink v6 is ESM-only and breaks CommonJS imports** - Importing Ink with `require('ink')` throws `require() of ES Module`. You must use `import` syntax and set `"type": "module"` in your `package.json`. This also means Ink v6 cannot be used in projects that are stuck on CommonJS without a build step.
237
2384. **`<Static>` items must have stable keys or they re-render** - The `<Static>` component renders each item exactly once and never updates it. If you pass items without stable `key` props or if you mutate the items array in place instead of appending, previously rendered lines can disappear or duplicate.
239
2405. **The app stays alive as long as stdin listeners or timers exist** - Ink's `render()` keeps the process running while there are pending timers, promises, or stdin listeners. Forgetting to call `clearInterval`, `clearTimeout`, or `exit()` from `useApp()` results in a CLI tool that hangs after the work is done.
241
2426. **`stdin.setRawMode is not a function`** - Running in non-TTY environment (piped input, CI). Check `isRawModeSupported` from `useStdin()` before enabling.
243
2447. **`React is not defined`** - Missing React import with JSX transform. Add `import React from 'react'` or configure JSX automatic runtime.
245
246## Verification
247
2481. Check Node version: `node -v` (must be >= 20)
2492. Check package.json for `"type": "module"` if using Ink v6+
2503. Run the CLI app: `node dist/index.js` or `npm start`
2514. Verify interactive input works in a real terminal (not piped)
2525. Verify the process exits cleanly after completion (no hanging)
2536. Verify no raw text is placed directly inside `<Box>` components
254
255## UI/UX 2026 Guidelines
256
257This skill also covers interface design, UX review, accessibility, responsive layouts, design systems, mobile/web UI, component behavior, interaction states, visual hierarchy, usability improvements, and frontend implementation guidance.
258
259**Workflow:**
2601. Start from the user task and information architecture, not decoration.
2612. Map key states: empty, loading, success, error, disabled, permission-limited, offline, and responsive variants.
2623. Apply accessibility requirements early: keyboard flow, focus visibility, labels, contrast, reduced motion, touch targets, text resizing, and semantic structure.
2634. Use design-system primitives where available; otherwise define tokens for spacing, color, type, elevation, radius, and motion.
2645. Design responsive layouts with stable dimensions and no text overlap across desktop and mobile.
2656. Validate with realistic content, long labels, error text, and touch/keyboard interaction.
2667. Deliver concrete implementation guidance, not vague aesthetic notes.
267
268**Quality Checklist:**
269- User can complete the core task quickly and repeatedly.
270- UI supports keyboard, screen readers, visible focus, and sufficient contrast.
271- Mobile and desktop layouts do not overlap or rely on fragile viewport-scaled text.
272- Controls use familiar affordances and expose state clearly.
273- Motion is purposeful and respects reduced-motion preferences.
274- Visual direction is intentional and consistent with the product domain.
275
276**Failure Handling:**
277If requirements conflict, prioritize usability, accessibility, and product fit over novelty. If a requested visual pattern harms readability or accessibility, explain the tradeoff and offer a better variant. Verify current platform guidance when building for Apple, Android, or a specific design system.
278
279**Current References:**
280- W3C WCAG 2.2: https://www.w3.org/TR/WCAG22/
281- W3C Understanding WCAG 2.2: https://www.w3.org/WAI/WCAG22/Understanding/intro
282- Apple Human Interface Guidelines: https://developer.apple.com/design/human-interface-guidelines
283- Material accessibility guidance: https://m2.material.io/design/usability/accessibility.html
284
285## References
286
287This skill does not ship a companion pack. Box/Text/hooks usage is in Procedure above. Full props, community components, and example apps:
288
289- Ink README (components + hooks): https://github.com/vadimdemedes/ink
290- Examples: https://github.com/vadimdemedes/ink/tree/master/examples
291
292## Related skills
293
294- frontend-developer: For general React web or React Native development.
295- shell-scripting: For simple shell scripts that just print output.