Back to list
enuno

ink

by enuno

Claude command & AI agent creation best practices and templates

6🍴 0📅 Jan 24, 2026

SKILL.md


name: ink version: "1.0.0" description: Ink - React for CLI applications, build interactive command-line interfaces using React components with Flexbox layout, hooks for input/focus, and testing support

Ink

Ink is a framework for building command-line interfaces using React components. It provides the same component-based UI building experience that React offers in the browser, but for command-line apps. The framework leverages Yoga, a Flexbox layout engine, enabling developers to use familiar CSS-like properties in terminal environments.

When to Use

  • Building interactive CLI applications with React
  • Creating terminal UIs with complex layouts
  • Building progress indicators and spinners for CLI tools
  • Creating multi-step wizards in the terminal
  • Building dev tools with rich terminal output
  • Creating dashboard-style CLI interfaces
  • Handling keyboard input in terminal apps
  • Building accessible CLI applications with screen reader support
  • Testing terminal UI components

Adoption

Ink is used by major projects including:

  • Shopify CLI
  • Gatsby
  • Prisma
  • Google's Gemini CLI
  • Cloudflare Wrangler
  • Parcel

Installation

npm install ink react

For TypeScript:

npm install ink react
npm install -D @types/react

Quick Start with Template

npx create-ink-app my-ink-cli
cd my-ink-cli
npm start

Basic Usage

import React from 'react';
import {render, Text} from 'ink';

const App = () => <Text color="green">Hello, Ink!</Text>;

render(<App />);

Counter Example

import React, {useState, useEffect} from 'react';
import {render, Text} from 'ink';

const Counter = () => {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setCounter(c => c + 1);
    }, 100);
    return () => clearInterval(timer);
  }, []);

  return <Text color="green">{counter} tests passed</Text>;
};

render(<Counter />);

Core Components

Text

The <Text> component handles all text rendering. Text content must be wrapped in this component.

import {Text} from 'ink';

// Basic text
<Text>Hello World</Text>

// With colors
<Text color="green">Success</Text>
<Text color="#ff0000">Error (hex)</Text>
<Text color="rgb(255, 128, 0)">Warning (rgb)</Text>

// With styles
<Text bold>Bold text</Text>
<Text italic>Italic text</Text>
<Text underline>Underlined</Text>
<Text strikethrough>Strikethrough</Text>
<Text inverse>Inverted colors</Text>
<Text dimColor>Dimmed text</Text>

// Combined styles
<Text bold color="cyan" backgroundColor="blue">
  Styled text
</Text>

Text Props

PropTypeDefaultDescription
colorstring-Text color (named, hex, rgb)
backgroundColorstring-Background color
dimColorbooleanfalseDim the color
boldbooleanfalseBold text
italicbooleanfalseItalic text
underlinebooleanfalseUnderlined text
strikethroughbooleanfalseStrikethrough text
inversebooleanfalseSwap foreground/background
wrapstring"wrap"Text wrapping mode

Wrap modes:

  • wrap - Wrap text (default)
  • truncate / truncate-end - Truncate at end with ellipsis
  • truncate-start - Truncate at start
  • truncate-middle - Truncate in middle

Box

<Box> is the primary layout component - a flexbox container for organizing UI elements.

import {Box, Text} from 'ink';

// Basic layout
<Box>
  <Text>Left</Text>
  <Text>Right</Text>
</Box>

// Column layout
<Box flexDirection="column">
  <Text>Top</Text>
  <Text>Bottom</Text>
</Box>

// With padding and margin
<Box padding={2} margin={1}>
  <Text>Padded content</Text>
</Box>

// With border
<Box borderStyle="round" borderColor="green" padding={1}>
  <Text>Bordered box</Text>
</Box>

// Centered content
<Box justifyContent="center" alignItems="center" height={10}>
  <Text>Centered</Text>
</Box>

Box Props - Dimensions

PropTypeDescription
widthnumber | stringWidth in spaces or percentage
heightnumber | stringHeight in lines or percentage
minWidthnumberMinimum width
minHeightnumberMinimum height

Box Props - Padding

PropTypeDescription
paddingnumberAll sides
paddingXnumberLeft and right
paddingYnumberTop and bottom
paddingTopnumberTop only
paddingBottomnumberBottom only
paddingLeftnumberLeft only
paddingRightnumberRight only

Box Props - Margin

PropTypeDescription
marginnumberAll sides
marginXnumberLeft and right
marginYnumberTop and bottom
marginTopnumberTop only
marginBottomnumberBottom only
marginLeftnumberLeft only
marginRightnumberRight only

Box Props - Flexbox

PropTypeDefaultDescription
flexDirectionstring"row"row, row-reverse, column, column-reverse
flexGrownumber0Grow factor
flexShrinknumber1Shrink factor
flexBasisnumber | string-Initial size
flexWrapstring"nowrap"nowrap, wrap, wrap-reverse
alignItemsstring-flex-start, center, flex-end
alignSelfstring-auto, flex-start, center, flex-end
justifyContentstring-flex-start, center, flex-end, space-between, space-around, space-evenly
gapnumber-Gap between children
columnGapnumber-Horizontal gap
rowGapnumber-Vertical gap

Box Props - Borders

PropTypeDescription
borderStylestring | objectBorder style
borderColorstringBorder color
borderTopbooleanShow top border
borderBottombooleanShow bottom border
borderLeftbooleanShow left border
borderRightbooleanShow right border

Border styles:

  • single - Single line (─│)
  • double - Double line (═║)
  • round - Rounded corners (╭╮╰╯)
  • bold - Bold line (━┃)
  • singleDouble - Single horizontal, double vertical
  • doubleSingle - Double horizontal, single vertical
  • classic - ASCII (+, -, |)

Box Props - Other

PropTypeDefaultDescription
displaystring"flex"flex or none
overflowstring"visible"visible or hidden
overflowXstring-Horizontal overflow
overflowYstring-Vertical overflow
backgroundColorstring-Background color

Newline

Insert line breaks within text.

import {Text, Newline} from 'ink';

<Text>
  First line
  <Newline />
  Second line
  <Newline count={2} />
  After two newlines
</Text>

Spacer

Flexible space that expands along the flex direction.

import {Box, Text, Spacer} from 'ink';

// Push items apart
<Box>
  <Text>Left</Text>
  <Spacer />
  <Text>Right</Text>
</Box>

// Equivalent to flexGrow: 1
<Box>
  <Text>Left</Text>
  <Box flexGrow={1} />
  <Text>Right</Text>
</Box>

Static

Render output once without updates. Perfect for logs or completed tasks.

import {Static, Box, Text} from 'ink';

const App = ({tests}) => (
  <>
    <Static items={tests}>
      {test => (
        <Box key={test.id}>
          <Text color="green">✔ {test.title}</Text>
        </Box>
      )}
    </Static>

    <Box marginTop={1}>
      <Text>Running more tests...</Text>
    </Box>
  </>
);

Props:

  • items - Array of items to render
  • children - Function (item, index) => ReactNode

Only new items trigger renders; previously rendered items are not updated.


Transform

Modify text output before rendering.

import {Transform, Text} from 'ink';

// Uppercase transform
<Transform transform={output => output.toUpperCase()}>
  <Text>hello world</Text>
</Transform>
// Output: HELLO WORLD

// Per-line transform (hanging indent)
<Transform transform={(line, index) =>
  index === 0 ? line : `  ${line}`
}>
  <Text>First line{'\n'}Second line{'\n'}Third line</Text>
</Transform>

Hooks

useInput

Capture keyboard input.

import {useInput, useApp} from 'ink';

const App = () => {
  const {exit} = useApp();

  useInput((input, key) => {
    // Character input
    if (input === 'q') {
      exit();
    }

    // Arrow keys
    if (key.upArrow) {
      // Move up
    }
    if (key.downArrow) {
      // Move down
    }

    // Modifiers
    if (key.ctrl && input === 'c') {
      exit();
    }

    // Special keys
    if (key.return) {
      // Enter pressed
    }
    if (key.escape) {
      // Escape pressed
    }
  });

  return <Text>Press 'q' to quit</Text>;
};

Key object properties:

PropertyDescription
upArrowUp arrow pressed
downArrowDown arrow pressed
leftArrowLeft arrow pressed
rightArrowRight arrow pressed
returnEnter key pressed
escapeEscape key pressed
ctrlCtrl modifier held
shiftShift modifier held
metaMeta/Cmd modifier held
tabTab key pressed
backspaceBackspace pressed
deleteDelete pressed
pageUpPage Up pressed
pageDownPage Down pressed

Options:

// Disable input handling conditionally
useInput(handler, {isActive: false});

useApp

Access app-level methods.

import {useApp} from 'ink';

const App = () => {
  const {exit} = useApp();

  const handleDone = () => {
    exit(); // Exit successfully
  };

  const handleError = () => {
    exit(new Error('Something went wrong')); // Exit with error
  };

  return <Text>App running...</Text>;
};

useStdin

Access stdin stream and raw mode.

import {useStdin} from 'ink';

const App = () => {
  const {stdin, isRawModeSupported, setRawMode} = useStdin();

  useEffect(() => {
    if (isRawModeSupported) {
      setRawMode(true);
    }
    return () => setRawMode(false);
  }, []);

  return <Text>Raw mode: {isRawModeSupported ? 'enabled' : 'not supported'}</Text>;
};

useStdout / useStderr

Access output streams.

import {useStdout, useStderr} from 'ink';

const App = () => {
  const {stdout, write} = useStdout();
  const {stderr, write: writeError} = useStderr();

  const logToStdout = () => {
    write('Direct stdout output\n');
  };

  const logToStderr = () => {
    writeError('Error output\n');
  };

  return <Text>Stream access available</Text>;
};

useFocus

Enable focus on components for Tab navigation.

import {useFocus, Box, Text} from 'ink';

const FocusableItem = ({label}) => {
  const {isFocused} = useFocus();

  return (
    <Box>
      <Text color={isFocused ? 'green' : 'gray'}>
        {isFocused ? '>' : ' '} {label}
      </Text>
    </Box>
  );
};

const App = () => (
  <Box flexDirection="column">
    <FocusableItem label="Option 1" />
    <FocusableItem label="Option 2" />
    <FocusableItem label="Option 3" />
  </Box>
);

Options:

useFocus({
  autoFocus: true,    // Focus on mount
  isActive: true,     // Enable/disable focus
  id: 'my-component'  // Unique identifier for programmatic focus
});

useFocusManager

Control focus programmatically.

import {useFocusManager, useInput} from 'ink';

const App = () => {
  const {
    enableFocus,
    disableFocus,
    focusNext,
    focusPrevious,
    focus
  } = useFocusManager();

  useInput((input, key) => {
    if (key.tab) {
      if (key.shift) {
        focusPrevious();
      } else {
        focusNext();
      }
    }
  });

  return (
    <Box flexDirection="column">
      <FocusableItem id="item-1" />
      <FocusableItem id="item-2" />
    </Box>
  );
};

render() API

import {render} from 'ink';

const {rerender, unmount, waitUntilExit, clear} = render(<App />, options);

Options

OptionTypeDefaultDescription
stdoutstreamprocess.stdoutOutput stream
stdinstreamprocess.stdinInput stream
stderrstreamprocess.stderrError stream
exitOnCtrlCbooleantrueExit on Ctrl+C
patchConsolebooleantrueIntercept console output
debugbooleanfalseDebug mode (no clear)
maxFpsnumber30Maximum frame rate
incrementalRenderingbooleanfalseOnly update changed lines

Instance Methods

// Re-render with new props
rerender(<App newProp={value} />);

// Unmount and exit
unmount();

// Wait for exit (returns promise)
await waitUntilExit();

// Clear rendered output
clear();

Testing

Use ink-testing-library for testing Ink components.

npm install --save-dev ink-testing-library
import {render} from 'ink-testing-library';
import App from './App';

test('renders greeting', () => {
  const {lastFrame} = render(<App name="World" />);
  expect(lastFrame()).toContain('Hello, World');
});

test('updates on input', () => {
  const {lastFrame, stdin} = render(<Counter />);

  expect(lastFrame()).toContain('Count: 0');

  stdin.write('i'); // Increment
  expect(lastFrame()).toContain('Count: 1');
});

Testing API

MethodDescription
lastFrame()Get last rendered output as string
framesArray of all rendered frames
stdinWritable stream for simulating input
rerender(element)Re-render with new element
unmount()Unmount the component

Accessibility

Ink supports ARIA attributes for screen reader compatibility.

<Box
  aria-role="list"
  aria-label="Menu options"
>
  <Box aria-role="listitem">
    <Text>Option 1</Text>
  </Box>
  <Box aria-role="listitem">
    <Text>Option 2</Text>
  </Box>
</Box>

ARIA Props

PropTypeDescription
aria-labelstringAccessible label
aria-hiddenbooleanHide from screen readers
aria-rolestringARIA role
aria-stateobjectState properties

Supported roles:

  • button, checkbox, radio, radiogroup
  • list, listitem
  • menu, menuitem
  • progressbar
  • tab, tablist
  • timer, toolbar, table

Screen Reader Detection

import {useIsScreenReaderEnabled} from 'ink';

const App = () => {
  const isScreenReaderEnabled = useIsScreenReaderEnabled();

  return (
    <Box aria-label="Main content">
      {isScreenReaderEnabled ? (
        <Text>Screen reader friendly content</Text>
      ) : (
        <Text>Visual content with colors</Text>
      )}
    </Box>
  );
};

Common Patterns

Loading Spinner

import React, {useState, useEffect} from 'react';
import {Text} from 'ink';

const Spinner = () => {
  const [frame, setFrame] = useState(0);
  const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];

  useEffect(() => {
    const timer = setInterval(() => {
      setFrame(f => (f + 1) % frames.length);
    }, 80);
    return () => clearInterval(timer);
  }, []);

  return <Text color="cyan">{frames[frame]} Loading...</Text>;
};

Progress Bar

import {Box, Text} from 'ink';

const ProgressBar = ({percent}) => {
  const width = 20;
  const filled = Math.round(width * percent / 100);
  const empty = width - filled;

  return (
    <Box>
      <Text color="green">{'█'.repeat(filled)}</Text>
      <Text color="gray">{'░'.repeat(empty)}</Text>
      <Text> {percent}%</Text>
    </Box>
  );
};
import React, {useState} from 'react';
import {Box, Text, useInput} from 'ink';

const Menu = ({items, onSelect}) => {
  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));
    }
    if (key.return) {
      onSelect(items[selected]);
    }
  });

  return (
    <Box flexDirection="column">
      {items.map((item, i) => (
        <Text key={item} color={i === selected ? 'green' : 'white'}>
          {i === selected ? '> ' : '  '}{item}
        </Text>
      ))}
    </Box>
  );
};

Two-Column Layout

import {Box, Text} from 'ink';

const TwoColumn = () => (
  <Box>
    <Box width="50%" borderStyle="single" padding={1}>
      <Text>Left Column</Text>
    </Box>
    <Box width="50%" borderStyle="single" padding={1}>
      <Text>Right Column</Text>
    </Box>
  </Box>
);

Resources


PackageDescription
ink-spinnerSpinner component
ink-text-inputText input component
ink-select-inputSelect/menu component
ink-tableTable component
ink-linkClickable links
ink-gradientGradient text
ink-big-textLarge ASCII text
ink-testing-libraryTesting utilities

Score

Total Score

50/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon