mcpField
I
inkjs-design

inkjs-design

github.com/akiojinDevelopment759 installsUpdated 34w ago
Repository

About inkjs-design

A design guide for developers building terminal UIs with Ink.js. It organizes React-based CLI interface patterns, practical examples, and troubleshooting references around screens, reusable parts, and input components.

Key features

  1. 01Screen, Part, and Common component patterns
  2. 02Keyboard input and focus guidance
  3. 03Multi-screen navigation patterns
  4. 04Responsive terminal layout guidance
  5. 05Performance and animation references
  6. 06Testing and troubleshooting references

Use cases

  1. 01Plan components for a new Ink.js CLI
  2. 02Resolve terminal layout or input conflicts
  3. 03Add navigation between CLI screens

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/akiojin/skills --skill inkjs-design

After installing, type/inkjs-designin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

examples/
component-examples.md
references/
animation-patterns.md
component-patterns.md
hooks-guide.md
ink-gotchas.md
input-handling.md
multi-screen-navigation.md
performance-optimization.md
responsive-layout.md
state-management.md
testing-patterns.md

Details

From the project README

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
IssueReference
Emoji width misalignmentink-gotchas.md
Ctrl+C called twiceink-gotchas.md
useInput conflictsink-gotchas.md
Layout breakingresponsive-layout.md
Screen navigationmulti-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.

Comments

Markdown supported