Skip to content

Repository files navigation

ink-canvas

A library for rendering Ink applications in the browser using Xterm.js.

ink-canvas bridges the gap between Node.js-based CLI UIs built with Ink and web-based terminal emulators. It provides custom stream implementations and Node.js environment mocks that allow Ink to render directly into an Xterm.js instance running in a React application.

Documentation Website

LicenseVersion

Features

  • 🖥️ Browser Compatibility: Run Ink applications entirely in the browser
  • 🎨 Xterm.js Integration: Leverages the power and styling of Xterm.js
  • 📐 Auto Resizing: Automatically handles terminal resizing and layout fitting
  • ⌨️ Input Handling: Captures keyboard input from the browser and forwards it to Ink
  • 🌊 Custom Streams: Built-in stdout, stderr, and stdin streams optimized for the browser
  • 🔌 Vite Plugin: Easy setup with automatic polyfill configuration

Table of Contents

Installation

Install ink-canvas and its peer dependencies:

# npm
npm install ink-canvas ink react @xterm/xterm @xterm/addon-fit
# yarn
yarn add ink-canvas ink react @xterm/xterm @xterm/addon-fit
# pnpm
pnpm add ink-canvas ink react @xterm/xterm @xterm/addon-fit

Quick Start

Here's a simple example to get you started:

importReact,{useState}from"react";import{Text,Box}from"ink";import{InkCanvas}from"ink-canvas";// Your Ink application componentconstMyInkApp=()=>(<BoxborderStyle="round"borderColor="green"><Text>Hello from Ink in the Browser! 👋</Text></Box>);// Main React componentconstApp=()=>{const[focused,setFocused]=useState(true);return(<divstyle={{height: "100vh",width: "100vw"}}><InkCanvasfocused={focused}terminalOptions={{fontSize: 14,theme: {background: "#1a1b26"},}}><MyInkApp/></InkCanvas></div>);};exportdefaultApp;

Configuration

Vite Configuration

To use ink-canvas with Vite, you need to configure polyfills for Node.js globals. The easiest way is to use the provided inkCanvasPolyfills plugin:

// vite.config.tsimport{defineConfig}from"vite";importreactfrom"@vitejs/plugin-react";import{inkCanvasPolyfills}from"ink-canvas/plugin";exportdefaultdefineConfig({plugins: [react(),inkCanvasPolyfills(),// Add this plugin],});

Webpack / Next.js Configuration

For Webpack-based projects like Next.js, use the InkCanvasWebpackPlugin. This plugin automatically configures module aliases, fallbacks, and provides global polyfills (process, Buffer) required by Ink.

Next.js Example (next.config.mjs):

import{InkCanvasWebpackPlugin}from"ink-canvas/plugin";/** @type {import('next').NextConfig} */constnextConfig={transpilePackages: ["ink-canvas"],// Ensure ink-canvas is transpiledwebpack: (config,{ isServer })=>{// Only apply the plugin for client-side buildsif(!isServer){config.plugins.push(newInkCanvasWebpackPlugin());}returnconfig;},};exportdefaultnextConfig;

Standard Webpack Example (webpack.config.js):

const{ InkCanvasWebpackPlugin }=require("ink-canvas/plugin");module.exports={// ...plugins: [newInkCanvasWebpackPlugin(),// ... other plugins],};

Polyfills Setup

The inkCanvasPolyfills plugin handles the following automatically:

  1. Process Shim: Redirects node:process imports to a browser-compatible shim
  2. Buffer Polyfill: Provides the Buffer global for binary data operations
  3. Global Object: Ensures the global object is available (maps to globalThis)

Manual Polyfill Configuration

If you prefer to configure polyfills manually or are using a different build tool, you need to:

  1. Install dependencies:
npm install vite-plugin-node-polyfills
  1. Configure your bundler to alias node:process to the ink-canvas process shim:
// vite.config.ts (manual configuration)import{defineConfig}from"vite";importreactfrom"@vitejs/plugin-react";import{nodePolyfills}from"vite-plugin-node-polyfills";exportdefaultdefineConfig({plugins: [react(),nodePolyfills({exclude: ["process"],// We use our own process shimglobals: {Buffer: true,global: true,},protocolImports: true,}),],resolve: {alias: {"node:process": "ink-canvas/shims/process",},},});

Development Mode (Library Development)

When developing the ink-canvas library itself, pass true to use local source files:

inkCanvasPolyfills(true);// Uses local shims/process.ts

API Reference

InkCanvas Component

The main component that wraps your Ink application.

import{InkCanvas}from"ink-canvas";

Props

PropTypeDefaultDescription
childrenReactNode-The Ink application to render
focusedbooleanfalseWhether the terminal captures keyboard input
colsnumberundefinedFixed number of columns. If omitted, fits container
rowsnumberundefinedFixed number of rows. If omitted, fits container
terminalOptionsITerminalOptions{}Configuration for the Xterm.js instance
onResize(dims: { cols: number, rows: number }) => void-Callback fired when terminal dimensions change
...divPropsHTMLAttributes<HTMLDivElement>-All other props are passed to the container div

Terminal Options

The terminalOptions prop accepts all Xterm.js ITerminalOptions except disableStdin. Common options include:

<InkCanvasterminalOptions={{// Font settingsfontSize: 16,fontFamily: "JetBrains Mono, Fira Code, monospace",fontWeight: "normal",fontWeightBold: "bold",// Cursor settingscursorStyle: "bar",// 'block' | 'underline' | 'bar'cursorBlink: true,// Theme (colors)theme: {background: "#1a1b26",foreground: "#a9b1d6",cursor: "#c0caf5",cursorAccent: "#1a1b26",selectionBackground: "#33467c",black: "#15161e",red: "#f7768e",green: "#9ece6a",yellow: "#e0af68",blue: "#7aa2f7",magenta: "#bb9af7",cyan: "#7dcfff",white: "#a9b1d6",},// Scrollbackscrollback: 1000,// Other optionsallowProposedApi: true,convertEol: false,}}><MyApp/></InkCanvas>

InkCanvasHandle

Access the underlying terminal instance using a ref:

import{useRef}from"react";import{InkCanvas,InkCanvasHandle}from"ink-canvas";constApp=()=>{constcanvasRef=useRef<InkCanvasHandle>(null);consthandleClick=()=>{// Access the Xterm.js terminalconstterminal=canvasRef.current?.terminal;// Get current dimensionsconstdimensions=canvasRef.current?.dimensions;console.log(`${dimensions?.cols}x${dimensions?.rows}`);// Access the Ink instanceconstinkInstance=canvasRef.current?.instance;};return(<InkCanvasref={canvasRef}><MyApp/></InkCanvas>);};

Handle Properties

PropertyTypeDescription
terminalTerminal | nullThe Xterm.js Terminal instance
dimensionsITerminalDimensions | nullCurrent terminal columns and rows
instanceInstance | nullThe Ink instance returned by render()

inkCanvasPolyfills Plugin

A Vite plugin that configures all necessary polyfills.

import{inkCanvasPolyfills}from"ink-canvas/plugin";

Function Signature

functioninkCanvasPolyfills(dev?: boolean): Plugin[];

Parameters

ParameterTypeDefaultDescription
devbooleanfalseIf true, uses local source paths (for library development)

Return Value

Returns an array of Vite plugins:

  1. vite-plugin-ink-canvas-polyfill: Aliases node:process to the process shim
  2. vite-plugin-node-polyfills: Provides Buffer and global polyfills

How It Works

Ink is designed for Node.js environments and relies on process.stdout, process.stdin, and other system APIs. ink-canvas provides:

1. Process Shim (shims/process.ts)

A browser-compatible mock of Node.js's process object:

  • process.env: Mocked environment variables with sensible defaults
  • process.stdout/stderr: Minimal stream mocks with TTY properties
  • process.stdin: Input stream mock
  • process.nextTick: Implemented using setTimeout
  • Other properties: platform, version, argv, cwd(), etc.

2. Custom Streams (utils/streams.ts)

TerminalWritableStream (stdout/stderr):

  • Receives ANSI escape codes and text from Ink
  • Converts LF (\n) to CRLF (\r\n) for proper Xterm.js rendering
  • Provides cursor manipulation methods (cursorTo, moveCursor, clearLine)
  • Emits resize events when terminal dimensions change

TerminalReadableStream (stdin):

  • Captures keyboard input from Xterm.js onData events
  • Buffers input and emits readable events for Ink to consume
  • Supports raw mode for character-by-character input

3. Canvas Component (components/Canvas.tsx)

A wrapper component that:

  • Automatically sizes itself to match stdout dimensions
  • Listens for resize events and updates accordingly
  • Ensures Ink's layout fills the entire terminal viewport

4. React Lifecycle Management (components/InkCanvas.tsx)

The InkCanvas component manages:

  • Xterm.js terminal initialization and cleanup
  • Stream creation and connection
  • Ink instance lifecycle (render, rerender, unmount)
  • Container auto-fitting with ResizeObserver
  • Focus state management

Examples

Basic Counter App

import{useState,useEffect}from"react";import{Text,Box,useInput}from"ink";import{InkCanvas}from"ink-canvas";constCounter=()=>{const[count,setCount]=useState(0);useInput((input,key)=>{if(input==="+"||key.upArrow){setCount((c)=>c+1);}elseif(input==="-"||key.downArrow){setCount((c)=>c-1);}elseif(input==="q"){// Handle quit}});return(<BoxflexDirection="column"padding={1}><Text>Count: {count}</Text><TextdimColor>Press +/- or ↑/↓ to change, q to quit</Text></Box>);};constApp=()=>(<InkCanvasfocusedstyle={{width: 400,height: 200}}><Counter/></InkCanvas>);

Interactive List with Scrolling

import{useState}from"react";import{Text,Box,useInput}from"ink";import{InkCanvas}from"ink-canvas";constitems=Array.from({length: 20},(_,i)=>`Item ${i+1}`);constScrollableList=()=>{const[selectedIndex,setSelectedIndex]=useState(0);useInput((_,key)=>{if(key.upArrow){setSelectedIndex((i)=>Math.max(0,i-1));}elseif(key.downArrow){setSelectedIndex((i)=>Math.min(items.length-1,i+1));}});return(<BoxflexDirection="column">{items.map((item,index)=>(<Textkey={item}color={index===selectedIndex ? "green" : undefined}bold={index===selectedIndex}>{index===selectedIndex ? "❯ " : " "}{item}</Text>))}</Box>);};constApp=()=>(<InkCanvasfocusedstyle={{width: "100%",height: "400px"}}><ScrollableList/></InkCanvas>);

Dynamic Resize Handling

import{useState}from"react";import{Text,Box}from"ink";import{InkCanvas}from"ink-canvas";constResizeDemo=()=>{const[size,setSize]=useState({cols: 0,rows: 0});return(<InkCanvasfocusedonResize={(dims)=>setSize(dims)}style={{width: "100%",height: "100vh"}}><BoxborderStyle="single"padding={1}><Text>
Terminal size: {size.cols} columns × {size.rows} rows
</Text></Box></InkCanvas>);};

Troubleshooting

Common Issues

"process is not defined" or "Cannot read properties of undefined (reading 'env')"

Ensure you have configured the polyfills correctly. Add inkCanvasPolyfills() to your Vite plugins:

// vite.config.tsimport{inkCanvasPolyfills}from"ink-canvas/plugin";exportdefaultdefineConfig({plugins: [react(),inkCanvasPolyfills()],});

Terminal not displaying / appears empty

Make sure the container element has explicit dimensions:

// ❌ Wrong - no dimensions<InkCanvas><MyApp/></InkCanvas>// ✅ Correct - explicit dimensions via style<InkCanvasstyle={{width: "100%",height: "400px"}}><MyApp/></InkCanvas>// ✅ Correct - explicit dimensions via CSS class<InkCanvasclassName="terminal-container"><MyApp/></InkCanvas>

Keyboard input not working

Ensure the focused prop is set to true:

<InkCanvasfocused={true}><MyApp/></InkCanvas>

Text wrapping incorrectly / layout issues

The terminal must be mounted before Ink can calculate layout. If you're seeing layout issues on initial render, try using the onResize callback to trigger a re-render:

const[ready,setReady]=useState(false);<InkCanvasonResize={()=>setReady(true)}>{ready&&<MyApp/>}</InkCanvas>;

Build errors with TypeScript

Ensure your tsconfig.json includes the necessary lib:

{
"compilerOptions": {
"lib": ["ES2020", "DOM", "DOM.Iterable"]
}
}

Getting Help

If you encounter issues not covered here, please:

  1. Check the GitHub Issues
  2. Create a new issue with a minimal reproduction

License

MIT

About

A library for rendering Ink applications in the browser

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages