| name | inkjs-design |
| description | 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)
|
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
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;
}, { isActive: isFocused });
3. Ctrl+C Handling
render(<App />, { exitOnCtrlC: false });
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
Advanced Topics
Troubleshooting
Examples
See examples/ for practical implementation examples.