
Ink: When Your CLI Needs a UI, But You Still Want React
Building interactive command-line applications often means wrestling with low-level terminal escape codes or brittle libraries. Ink offers a fascinating alternative, letting you leverage your React skills to build rich, dynamic CLIs.
by Sunil Band
Your CLI Deserves Better Than console.log
We've all been there: you're building a tool, a script, or a deployment helper, and it needs to provide some feedback to the user. Maybe it's a progress bar, a live log of steps, or even a simple form to collect input. The default approach is usually a flurry of console.log statements, perhaps some ASCII art for a rudimentary progress spinner. It gets the job done, but it's often clunky, hard to update dynamically, and feels a decade behind modern UI development.
This is where Ink steps in. It's a library that lets you build interactive command-line interfaces (CLIs) using the same declarative component model you're used to with React. Forget about manually managing cursor positions or figuring out ANSI escape codes. Ink abstracts all that away, letting you focus on the logic and presentation with familiar React patterns.
The real power of Ink isn't just that it uses React; it's that it brings the entire React ecosystem with it. State management, hooks, component composition – all the things that make building complex UIs on the web enjoyable and maintainable are now available for your CLI. This means your CLI can be just as sophisticated and user-friendly as a web application, providing a much richer experience than a stream of static text.
Why Use React for a CLI?
"React for a CLI?" Some of you are probably thinking that's overkill. And for a simple script, you're right. But think about more complex scenarios:
- Long-running processes with live feedback: Imagine a build process that shows real-time progress, currently running tasks, and error messages that dynamically appear and disappear.
- Interactive wizards or forms: Instead of prompting the user line by line, you could render a multi-step form with validation, dropdowns, and even dynamic fields.
- Dashboards for local services: A tool that monitors local Docker containers or background services, updating their status live in your terminal.
In these cases, the traditional console.log approach quickly becomes unmanageable. You end up writing brittle code to clear lines, move cursors, and manage complex state transitions. Ink provides a robust, battle-tested framework for exactly these challenges, making your CLI tools feel polished and professional.
Getting Started with Ink: A Live Progress Bar
Let's dive into a practical example. We'll create a simple CLI application that simulates a long-running task and displays a live progress bar, along with a list of dynamically updating messages. This is a common pattern where Ink truly shines.
First, you'll need a basic Node.js project. If you're using TypeScript (and you should be for anything beyond a trivial script), set up a tsconfig.json and install typescript and ts-node.
npm init -y
npm install react ink @types/react @types/node ts-node typescript --save-devNow, let's create our main application file, src/app.tsx:
import React, { useState, useEffect } from 'react';
import { render, Text, Box, ProgressBar } from 'ink';
// A simple component to display a progress bar and messages
const App = () => {
const [progress, setProgress] = useState(0);
const [messages, setMessages] = useState<string[]>([]);
const [isDone, setIsDone] = useState(false);
useEffect(() => {
if (isDone) return;
const interval = setInterval(() => {
setProgress(prevProgress => {
const newProgress = Math.min(prevProgress + 10, 100);
if (newProgress === 100) {
clearInterval(interval);
setIsDone(true);
setMessages(prev => [...prev, 'Task completed successfully!']);
}
return newProgress;
});
// Add a new message every few seconds
if (Math.random() > 0.6 && progress < 90) {
setMessages(prev => [...prev, `Processing step ${messages.length + 1}...`]);
}
}, 500); // Update every half second
return () => clearInterval(interval);
}, [progress, isDone, messages.length]);
return (
<Box flexDirection="column" padding={1} borderStyle="round" borderColor="green">
<Text color="cyan" bold>My Awesome CLI Tool</Text>
<Box marginY={1}>
<Text>Progress: </Text>
{/* The Ink ProgressBar component */}
<ProgressBar
value={progress}
barCharacter="█"
leftBracket="["
rightBracket="]"
gradient={['red', 'yellow', 'green']}
/>
<Text> {progress}%</Text>
</Box>
{/* Dynamically rendered messages */}
<Box flexDirection="column" borderStyle="single" borderColor="gray" padding={0} width="60%">
<Text color="magenta" bold>Logs:</Text>
{messages.map((msg, index) => (
<Text key={index} dimColor={isDone && index === messages.length - 1 ? false : true}>- {msg}</Text>
))}
{!isDone && <Text dimColor>Waiting for next step...</Text>}
</Box>
{isDone && <Text color="green" bold marginTop={1}>All tasks done!</Text>}
</Box>
);
};
// Render the React component to the terminal
render(<App />);Let's break down a few key parts of this code:
-
render(<App />): Just likeReactDOM.renderfor web apps, this is the entry point for your Ink application. It renders your root React component to the terminal. -
Text,Box,ProgressBar: These are Ink's built-in components.Textis for displaying text (obviously), andBoxis analogous to adivin the browser, used for layout. Ink uses a subset of Yoga layout (Flexbox) for arranging components, which is incredibly powerful for building structured CLIs. -
useStateanduseEffect: Standard React hooks.progressandmessagesare updated over time, anduseEffecthandles the interval that simulates the work, ensuring the component re-renders to reflect the new state. -
ProgressBar: This is a great example of a specialized Ink component. It takes avalueand renders a visual progress bar, even supporting gradients.
To run this, add a script to your package.json:
{
"name": "my-ink-cli",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "ts-node src/app.tsx"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/node": "^20.12.7",
"@types/react": "^18.2.79",
"ink": "^4.4.1",
"react": "^18.2.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
}
}Now, run npm start in your terminal. You'll see a dynamically updating progress bar and a log of messages, all rendered directly in your terminal, without a single console.log in sight for the output.
Beyond Basics: Input and Advanced Layout
Ink isn't just for displaying output. It also provides components for handling user input, like TextInput for free-form text, SelectInput for dropdowns, and ConfirmInput for yes/no prompts. This means you can build truly interactive experiences.
Furthermore, its Flexbox-based layout system (via the Box component) allows for complex arrangements. You can split your screen into multiple panels, each with its own content, and manage their individual states. Think of a git status output that's dynamically updating, or a build log that shows different stages in different areas of the terminal.
Trade-offs and Considerations
While Ink is powerful, it's not a silver bullet for every CLI. Here are a few things to keep in mind:
- Overhead for simple scripts: For basic
console.logstyle scripts, introducing React and Ink is unnecessary overhead. Only reach for it when you genuinely need dynamic UI capabilities. - Bundle size: While Ink itself is relatively small, adding React and all its dependencies will make your CLI larger than a pure Node.js script. If you're building a tool meant for extremely fast startup times in resource-constrained environments, this might be a factor.
- Terminal limitations: You're still constrained by the terminal's capabilities. You don't have CSS, rich media, or the full fidelity of a browser. Ink does an incredible job of making the most of what's available, but don't expect a full web app in your terminal.
- Learning curve (if new to React): If you're not already comfortable with React's component model, hooks, and state management, there will be a learning curve. However, for a seasoned React developer, it's remarkably intuitive.
Wrapping up
If you've ever found yourself frustrated by the limitations of traditional CLI output, or wished you could bring the elegance of modern UI development to your terminal tools, Ink is definitely worth exploring. It's a fantastic example of how a well-designed abstraction can unlock entirely new possibilities, turning what used to be a tedious task into something genuinely enjoyable.
My advice? Think of a small utility script you've always wanted to make a bit more user-friendly – perhaps a local development server dashboard, or a script that automates some repetitive task with interactive prompts. Clone the example above, play with the ProgressBar and Text components, and then try integrating TextInput to collect some user input. You'll quickly realize how much more engaging your CLI tools can become with the power of React behind them.

Internationalization in Next.js: Beyond the Basic `i18n.js` File
Setting up i18n in Next.js often starts with a simple JSON file, but real-world applications quickly outgrow that. `next-intl` offers a robust solution that integrates deeply with Next.js features, including Server Components, to manage translations more effectively and avoid common pitfalls.

Beyond the Canvas: When Your Frontend Becomes an AI Art Studio
Stable Diffusion, Midjourney, DALL-E 3 — these names are everywhere. But for frontend developers, the interaction often stops at calling an API or embedding a widget. InvokeAI, however, presents a different paradigm: bringing the full power of an AI art engine directly into a sophisticated web appli

Beyond the Marketing: When a No-Code Editor Becomes Your Component Playground
I've been wary of 'no-code' tools. They often promise the moon but deliver a walled garden, abstracting away too much and leaving you stranded when you need real customizability. GrapesJS, however, isn't just another drag-and-drop editor; it's a web builder framework. This distinction fundamentally


















