mrkeyoor.com_
Wed 23 Sept 00:33 UTC
npmCLI & Toolingupdated 22 Sept 2026

ink review

Ink 7.1.1 is a React renderer for terminal interfaces. Components return Text, Box, Static, and other terminal primitives; Ink reconciles that tree, uses Yoga for cell-based Flexbox layout, and updates ANSI output in place. Hooks cover keyboard input, paste events, focus, terminal dimensions, cursor placement, and app shutdown. It does not parse command arguments or run shell commands for you. Version 7.1 added suspendTerminal() so a child process can temporarily own the TTY, and 7.1.1 fixes a completed Static line being erased after a full-screen clear while adding x and y coordinates to measureElement().

Verdict

Ink 7.1.1 installed in 4.3 seconds with 38 packages, 23 MB on disk, and 0 audit findings in our sandbox, while require() and browser bundling both failed. It fits a real interactive terminal application whose team already works in React; a prompt sequence or log-producing command should use a smaller CLI-specific tool.

We installed it

Lab card: what happened when we installed inkScreenshot of ink documentation
Install✓ · 4.3s38 packages on disk · 23 MB
Import½ESM import works · require() fails · ESM package with exports map
Browsern/acould not be bundled for the browser (Node-only code, most likely)
Typesno TypeScript types found
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does ink install cleanly?

Yes. In a fresh container with an empty cache, npm install ink finished in 4 seconds, leaving 38 packages and 23 MB on disk. npm audit reported no known vulnerabilities.

Can ink run in a browser?

Not directly: esbuild could not bundle it for the browser in our run, which normally means it depends on Node built-ins. Use it on the server, or find a browser-targeted alternative.

Does ink work with both ESM and CommonJS?

ESM only. import 'ink' worked, require('ink') failed in our run, so CommonJS projects need a dynamic import or a build step.

Does ink include TypeScript types?

No type declarations were found in our install, so TypeScript users need their own declarations.

ink or @clack/prompts: which should you use?

@clack/prompts: Use it for polished questions, confirmations, selections, and progress in a short command flow. Ink 7.1.1 installed in 4.3 seconds with 38 packages, 23 MB on disk, and 0 audit findings in our sandbox, while require() and browser bundling both failed.

When should you not use ink?

The command asks a few questions and exits. @clack/prompts or Enquirer provides prompt controls without a React renderer and 38 installed packages.

API stability4/5Ink 7.1.1 retains the established render(), Text, Box, Static, useInput, useApp, and focus model while extending terminal ownership through suspendTerminal(). Patch 7.1.1 changes measureElement() by adding x and y coordinates and fixes Static output after a full clear. Major-version requirements still move with React and Node: this release needs Node 22 plus React 19.2 peers, uses ESM, and gives CommonJS callers no working require() path in our check.
Docs5/5The tagged README documents installation, app lifetime, every built-in component, input and focus hooks, terminal sizing, render options, testing, CI, and screen-reader output. It states exact behavior for Static items, raw mode, redirected stdout, suspendTerminal(), and waitUntilExit(). The main documentation trap is printed near installation: the repository README describes the upcoming version, so users should pair it with the 7.1.1 release tag before copying a new API.
Maintenance5/5The repository was pushed on August 25, 2026, remains unarchived, and GitHub reports 33 open issues and pull requests. Release 7.1.1 shipped on July 16 after 7.1.0 in June, with fixes for full-clear frames, Windows terminal width behavior, resize listeners, and error rendering across the recent 7.0.x and 7.1.x releases. Work is current and aimed at terminal-specific failures rather than cosmetic release churn.
Ecosystem5/5npm counted 6,109,340 Ink downloads for August 18 through 24, 2026, and GitHub reports 39,732 stars. The README lists current users such as Claude Code, Gemini CLI, GitHub Copilot CLI, Wrangler, Prisma, and Shopify CLI. Third-party packages cover spinners, text input, links, tables, and selection, though adding them increases a base install that already left 38 packages in our sandbox.

Use it if

  • The command has persistent views, keyboard navigation, live progress, and enough shared UI state to benefit from React components.
  • Your team already knows React hooks and wants that programming model for a terminal dashboard or interactive developer tool.
  • Cell-based Flexbox layout is easier to maintain than handwritten cursor movement and line-clearing sequences.
  • The interface needs a tested plain-output path alongside an interactive TTY experience.
Skip it if

Setup reality

We installed Ink 7.1.1 in a fresh Node 22 Bookworm sandbox in 4.3 seconds. The result was 38 packages and 23 MB on disk, and npm audit found 0 known vulnerabilities. Ink declares 25 direct dependencies and 3 peers; its own unpacked package is 1104 KB. ESM import worked, while require() failed under Node 22.23.2. Our package check found no bundled TypeScript declarations.

Install React beside Ink and satisfy the React 19.2, @types/react 19.2, and optional React DevTools peer ranges declared by 7.1.1. JSX still needs a compiler or runtime transform. The project scaffold configures that path; a manual JavaScript setup needs Babel plus the React preset. No credentials are involved. Text must sit inside Text, and each Box is a Yoga Flexbox container measured in terminal cells rather than CSS pixels.

Our esbuild browser bundle failed, which matches a Node-only terminal renderer. A live app also needs stdin and stdout that behave like a terminal. Before enabling useInput, check isRawModeSupported; pipes and many CI jobs cannot enter raw mode. In non-interactive mode, Ink omits ANSI erase commands, synchronized output, resize handling, and keyboard protocol detection. For automation, an explicit JSON or line-oriented output branch is easier to consume than the last rendered frame.

Timers, input listeners, sockets, and pending promises keep the Node process alive. Clean them up in React effects, then call exit() or unmount when the command is finished. Version 7.1's suspendTerminal() can hand the TTY to an editor or child command, but nested suspension throws and the handoff only occurs in an interactive TTY. Static output is append-only: changing an item already rendered does not rewrite its old terminal line.

Patterns

Render a terminal component render-cli

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

function App() {
  return <Text color="green">Ready</Text>;
}

const app = render(<App />);
await app.waitUntilExit();

Ink 7 is ESM in our package check, so run this through an ESM-aware JSX or TypeScript build instead of require().

Arrange a status row with Box layout-status-row

import {Box, Text} from 'ink';

export function StatusRow() {
  return (
    <Box width={60} gap={2}>
      <Box width={16}><Text bold>compile</Text></Box>
      <Box flexGrow={1}><Text color="yellow">running</Text></Box>
    </Box>
  );
}

Box uses Yoga and terminal-cell widths. Every visible string still belongs inside a Text component.

Update progress and clear its timer update-progress

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

export function Progress() {
  const [seconds, setSeconds] = useState(0);
  useEffect(() => {
    const id = setInterval(() => setSeconds(value => value + 1), 1000);
    return () => clearInterval(id);
  }, []);
  return <Text>{seconds}s elapsed</Text>;
}

The 1000 ms interval keeps Node alive. Returning clearInterval from the effect lets unmount finish the command.

Move selection and quit from the keyboard handle-keyboard

import {Text, useApp, useInput} from 'ink';

export function Controls() {
  const {exit} = useApp();
  useInput((input, key) => {
    if (input === 'q' || key.escape) exit();
    if (key.upArrow) move(-1);
    if (key.downArrow) move(1);
  });
  return <Text dimColor>Up/down move; q quits</Text>;
}

useInput needs raw-mode-capable stdin. Check useStdin().isRawModeSupported before mounting this control in a piped or CI environment.

Receive a pasted block as one value capture-paste

import {Text, usePaste} from 'ink';

export function PasteBox() {
  usePaste(text => importJson(text));
  return <Text>Paste JSON</Text>;
}

usePaste uses bracketed paste mode and keeps embedded newlines in one callback. Validate the pasted string before parsing or executing anything from it.

Keep finished tasks above a live row keep-completed-lines

import {Static, Text} from 'ink';

export function Results({finished, current}) {
  return (
    <>
      <Static items={finished}>
        {item => <Text key={item.id}>✓ {item.name}</Text>}
      </Static>
      <Text>Running: {current}</Text>
    </>
  );
}

Static writes only items appended after earlier renders. Mutating an item already emitted will not change its existing terminal line.

Give the terminal to a child command suspend-for-child-process

import {Text, useApp, useInput} from 'ink';
import {spawn} from 'node:child_process';

export function EditorControl() {
  const {suspendTerminal} = useApp();
  useInput(input => {
    if (input !== 'e') return;
    void suspendTerminal(() => new Promise((resolve, reject) => {
      const child = spawn('git', ['commit'], {stdio: 'inherit'});
      child.once('exit', code => code === 0 ? resolve() : reject(new Error(`git exited ${code}`)));
    }));
  });
  return <Text>Press e to open git commit</Text>;
}

suspendTerminal() arrived in Ink 7.1. It throws if another suspension is active and performs no TTY handoff in non-interactive mode.

Read a Box position after layout measure-layout

import React, {useEffect, useRef} from 'react';
import {Box, measureElement} from 'ink';

export function Panel() {
  const ref = useRef(null);
  useEffect(() => {
    console.log(measureElement(ref.current));
  }, []);
  return <Box ref={ref} width={20} height={4} />;
}

Version 7.1.1 returns x, y, width, and height. Calling measureElement during render yields four zero values because layout has not run.

Switch layout at a terminal width handle-terminal-width

import {Text, useWindowSize} from 'ink';

export function WidthMode() {
  const {columns, rows} = useWindowSize();
  return <Text>{columns < 80 ? 'compact' : 'wide'} ({columns}x{rows})</Text>;
}

Non-interactive mode disables resize handling. Treat the reported dimensions as a live-TTY feature rather than a dependable CI signal.

Expose a focusable terminal control manage-focus

import {Text, useFocus} from 'ink';

export function Choice({id, label}) {
  const {isFocused} = useFocus({id});
  return <Text inverse={isFocused}>{isFocused ? '> ' : '  '}{label}</Text>;
}

useFocus tracks which component owns focus, but the component must still handle its keys with useInput.

Return a value when the app exits return-result

const instance = render(<Picker />);
const chosenId = await instance.waitUntilExit();
console.log(chosenId);

// Inside Picker:
// useApp().exit(selectedId);

exit(value) resolves waitUntilExit with that value. Passing an Error rejects the promise and should be handled by the command entry point.

Emit plain data when stdout is redirected support-redirected-output

import process from 'node:process';

if (process.stdout.isTTY) {
  const app = render(<Dashboard />);
  await app.waitUntilExit();
} else {
  process.stdout.write(JSON.stringify(await readStatus()) + '\n');
}

Ink writes only the final changing frame at unmount in non-interactive mode. A separate JSON branch gives scripts a stable contract.

Alternatives

PackageRegistryPick it when
@clack/promptsnpmUse it for polished questions, confirmations, selections, and progress in a short command flow.
enquirernpmUse it when the interface is a sequence of prompts and custom prompt types matter more than persistent layout.
terminal-kitnpmUse it for direct terminal control, widgets, drawing, and input without React reconciliation.
blessednpmUse it for an older widget-style terminal UI when maintaining its callback-oriented API is acceptable.

More cli & tooling guides

chalk · commander · typescript · esbuild · yargs · click · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.