inkjs-design

akiojin's avatarfrom akiojin

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)

⭐0stars🔀0forks📁View on GitHub🕐Updated Dec 26, 2025

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.
nameinkjs-design
description|

Ink.js Design

Comprehensive guide for building terminal UIs with Ink.js (React for CLI).

Quick Start

Creating a New Component

  1. Determine component type: Screen / Part / Common
  2. Reference component-patterns.md for similar patterns
  3. Add type definitions
  4. Implement component
  5. 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

Advanced Topics

Troubleshooting

Examples

See examples/ for practical implementation examples.