Skip to main content
Technology & EngineeringCli Development197 lines

Ink React CLI

Build rich interactive terminal UIs using Ink, a React renderer for the command line

Quick Summary21 lines
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 lines
Paste into your CLAUDE.md or agent config

Ink — 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 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.

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.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.

  • 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 blessed or direct ANSI escape sequences creates rendering conflicts where both systems fight over cursor position and screen clearing.

Common Pitfalls

  • 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.

Install this skill directly: skilldb add cli-development-skills

Get CLI access →