From routine
Builds CLI apps using Ink (React renderer for terminals), ink-ui components (TextInput, Select, Spinner), and create-ink-app scaffolding for interactive terminal UIs and JSX in command-line tools.
npx claudepluginhub delexw/claude-code-miscThis skill uses the workspace's default tool permissions.
Ink is a React renderer for terminal applications. It uses Yoga (flexbox) for layout and renders to stdout. Every element is a flex container — think `<div style="display: flex">` for the terminal.
Build terminal UIs for React CLI apps using Ink component patterns: layouts, lists with icons, status messages, progress indicators, and collapsible sections.
Renders JSON specs into interactive terminal UIs using Ink components like Box, Text, Tables, Charts, Spinners, with data binding and actions. Use for terminal apps, component catalogs, or AI-generated UIs.
Provides TUI performance and UX guidelines for TypeScript CLI tools using Ink and Clack. Applies to interactive prompts, terminal rendering, keyboard input, and developer tooling.
Share bugs, ideas, or general feedback.
Ink is a React renderer for terminal applications. It uses Yoga (flexbox) for layout and renders to stdout. Every element is a flex container — think <div style="display: flex"> for the terminal.
# Scaffold a new project
npx create-ink-app my-cli # JavaScript
npx create-ink-app --typescript my-cli # TypeScript
Or add to an existing project:
npm install ink react
npm install @inkjs/ui # Optional: pre-built UI components
Ink apps are React component trees rendered via render(). The process stays alive while there's work in the event loop. Exit via Ctrl+C, useApp().exit(), or instance.unmount().
import React, {useState} from 'react';
import {render, Text, Box} from 'ink';
function App() {
const [count, setCount] = useState(0);
return (
<Box flexDirection="column">
<Text>Count: {count}</Text>
<Text color="green">Press q to quit</Text>
</Box>
);
}
render(<App />);
Read these for detailed API documentation and examples:
Box, Text, Newline, Spacer, Static, Transform) with full propsuseInput, useApp, useFocus, useFocusManager, useStdin, useStdout, useStderr, useWindowSize, useBoxMetrics, useCursor, usePaste)@inkjs/ui components (TextInput, Select, MultiSelect, Spinner, ProgressBar, Alert, Badge, StatusMessage, ConfirmInput, EmailInput, PasswordInput, OrderedList, UnorderedList) with props and themingEvery element is a flex container. Use <Box> for layout with standard flex props: flexDirection, justifyContent, alignItems, gap, padding, margin, etc. Percentage widths/heights are supported.
<Text>All string content must be wrapped in <Text>. Direct string children of <Box> will error. Nest <Text> inside <Text> for inline styling:
<Text>
Hello <Text bold color="green">World</Text>
</Text>
<Static> for Permanent OutputUse <Static> for output that should persist above the interactive area (like log lines). Content rendered in <Static> is written once and never re-rendered:
<Static items={logs}>
{(log, i) => <Text key={i}>{log}</Text>}
</Static>
Use useInput hook — not DOM events:
import {useInput, useApp} from 'ink';
function App() {
const {exit} = useApp();
useInput((input, key) => {
if (input === 'q') exit();
if (key.return) handleSubmit();
});
return <Text>Press q to quit</Text>;
}
<Box> supports border styles: "single", "double", "round", "bold", "singleDouble", "doubleSingle", "classic".
<Box borderStyle="round" borderColor="green" padding={1}>
<Text>Bordered content</Text>
</Box>
Use ink-testing-library:
import {render} from 'ink-testing-library';
const {lastFrame, stdin} = render(<App />);
expect(lastFrame()).toContain('Hello');
stdin.write('q'); // simulate input
const instance = render(<App />, {
stdout: process.stdout, // custom writable stream
stdin: process.stdin, // custom readable stream
stderr: process.stderr, // custom writable stream
exitOnCtrlC: true, // default
patchConsole: true, // intercept console.log
debug: false,
maxFps: 30,
incrementalRendering: false, // only re-render changed lines
concurrent: false, // React concurrent mode (Suspense, useTransition)
interactive: true, // auto-detected; false in CI
isScreenReaderEnabled: false, // or set INK_SCREEN_READER=true
onRender: ({renderTime}) => {},// callback after each render
kittyKeyboard: {mode: 'auto'}, // 'auto' | 'enabled' | 'disabled'
});
await instance.waitUntilExit();
import {renderToString} from 'ink';
const output = renderToString(<App />, {columns: 80});
<Box> — Always wrap text in <Text>useInput hook insteadkey prop in <Static> — Items need unique keysuseInput requires raw mode (automatic in render(), but manual in tests)isDisabled prop on ink-ui components or isActive on useInput/useFocus to manage which component receives input