inkjs-design
Ink.js (React for CLI) design and implementation guide.Use when:(1) Creating or modifying Ink.js components(2) Implementing Ink-specific hooks (useInput, useApp, useFocus)(3) Handling emoji/icon width issues (string-width workarounds)(4) Building terminal-responsive layouts(5) Managing multi-screen navigation(6) Implementing animations (spinners, progress bars)(7) Optimizing performance (React.memo, useMemo)(8) Handling keyboard input and shortcuts(9) Testing CLI UI (ink-testing-library)
When & Why to Use This Skill
This Claude skill serves as a comprehensive design and implementation guide for building sophisticated terminal user interfaces (TUIs) using Ink.js (React for CLI). It streamlines the development process by providing structured patterns for component architecture, responsive terminal layouts, and complex keyboard input handling, ensuring high-performance and maintainable CLI applications.
Use Cases
- Architecting scalable CLI tools using a structured Screen/Part/Common component hierarchy.
- Implementing interactive terminal dashboards with multi-screen navigation and real-time animations like spinners and progress bars.
- Resolving technical terminal rendering issues such as emoji width misalignment and conflicting keyboard input listeners.
- Optimizing CLI performance using React memoization patterns and custom hooks tailored for terminal environments.
| name | inkjs-design |
|---|---|
| description | | |
Ink.js Design
Comprehensive guide for building terminal UIs with Ink.js (React for CLI).
Quick Start
Creating a New Component
- Determine component type: Screen / Part / Common
- Reference component-patterns.md for similar patterns
- Add type definitions
- Implement component
- Write tests
Common Issues & Solutions
| Issue | Reference |
|---|---|
| Emoji width misalignment | ink-gotchas.md |
| Ctrl+C called twice | ink-gotchas.md |
| useInput conflicts | ink-gotchas.md |
| Layout breaking | responsive-layout.md |
| Screen navigation | multi-screen-navigation.md |
Directory Conventions
src/cli/ui/
├── components/
│ ├── App.tsx # Root component with screen management
│ ├── common/ # Common input components (Select, Input)
│ ├── parts/ # Reusable UI parts (Header, Footer)
│ └── screens/ # Full-screen components
├── hooks/ # Custom hooks
├── utils/ # Utility functions
└── types.ts # Type definitions
Component Classification
Screen (Full-page views)
- Represents a complete screen/page
- Handles keyboard input via
useInput - Implements Header/Content/Footer layout
- Manages screen-level state
Part (Reusable elements)
- Reusable UI building blocks
- Optimized with
React.memo - Stateless/pure components preferred
- Accept configuration via props
Common (Input components)
- Basic input components
- Support both controlled and uncontrolled modes
- Handle focus management
- Provide consistent UX
Essential Patterns
1. Icon Width Override
Fix string-width v8 emoji width calculation issues:
const WIDTH_OVERRIDES: Record<string, number> = {
"⚡": 1, "✨": 1, "🐛": 1, "🔥": 1, "🚀": 1,
"🟢": 1, "🟠": 1, "✅": 1, "⚠️": 1,
};
const getIconWidth = (icon: string): number => {
const baseWidth = stringWidth(icon);
const override = WIDTH_OVERRIDES[icon];
return override !== undefined ? Math.max(baseWidth, override) : baseWidth;
};
2. useInput Conflict Avoidance
Multiple useInput hooks all fire - use early return or isActive:
useInput((input, key) => {
if (disabled) return; // Early return when inactive
// Handle input...
}, { isActive: isFocused });
3. Ctrl+C Handling
render(<App />, { exitOnCtrlC: false });
// In component
const { exit } = useApp();
useInput((input, key) => {
if (key.ctrl && input === "c") {
cleanup();
exit();
}
});
4. Dynamic Height Calculation
const { rows } = useTerminalSize();
const HEADER_LINES = 3;
const FOOTER_LINES = 2;
const contentHeight = rows - HEADER_LINES - FOOTER_LINES;
const visibleItems = Math.max(5, contentHeight);
5. React.memo with Custom Comparator
function arePropsEqual<T>(prev: Props<T>, next: Props<T>): boolean {
if (prev.items.length !== next.items.length) return false;
for (let i = 0; i < prev.items.length; i++) {
if (prev.items[i].value !== next.items[i].value) return false;
}
return prev.selectedIndex === next.selectedIndex;
}
export const Select = React.memo(SelectComponent, arePropsEqual);
6. Multi-Screen Navigation
type ScreenType = "main" | "detail" | "settings";
const [screenStack, setScreenStack] = useState<ScreenType[]>(["main"]);
const currentScreen = screenStack[screenStack.length - 1];
const navigateTo = (screen: ScreenType) => {
setScreenStack(prev => [...prev, screen]);
};
const goBack = () => {
if (screenStack.length > 1) {
setScreenStack(prev => prev.slice(0, -1));
}
};
Detailed References
Core Patterns
- Component Patterns - Screen/Part/Common architecture
- Hooks Guide - Custom hook design patterns
Advanced Topics
- Multi-Screen Navigation - Screen stack management
- Animation Patterns - Spinners and progress bars
- State Management - Complex state patterns
- Responsive Layout - Terminal size handling
- Performance Optimization - Optimization techniques
- Input Handling - Keyboard input patterns
Troubleshooting
- Ink Gotchas - Common issues and solutions
- Testing Patterns - ink-testing-library usage
Examples
See examples/ for practical implementation examples.