Claude-skill-registry ink-cli
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.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/ink-cli" ~/.claude/skills/majiayu000-claude-skill-registry-ink-cli && rm -rf "$T"
manifest:
skills/data/ink-cli/SKILL.mdsource content
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><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
<Box>// 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
<Static>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>
<Newline><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 transform={output => output.toUpperCase()}> <Text>hello</Text> {/* Renders: HELLO */} </Transform>
Essential Hooks
useInput
- Keyboard Input
useInputimport {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
useAppconst {exit} = useApp(); exit(); // Clean exit exit(error); // Exit with error
useFocus
- Focus Management
useFocusconst Item = ({label}) => { const {isFocused} = useFocus(); return ( <Text color={isFocused ? 'green' : 'white'}> {isFocused ? '>' : ' '} {label} </Text> ); };
Options:
autoFocus, isActive, id
useFocusManager
- Programmatic Focus
useFocusManagerconst {focusNext, focusPrevious, focus} = useFocusManager(); focus('specific-id');
useStdout
/ useStderr
- Direct Output
useStdoutuseStderrconst {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
: Never put raw text directly in<Text><Box> - Use
for logs: Prevents re-rendering of completed output<Static> - 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
in render optionsexitOnCtrlC: false