Building rich command-line interfaces with Ink and ReactMarch 6, 2019

Ink is a library for building and testing command-line applications using React components. Since it acts as a React renderer, you can use everything that React has to offer: hooks, class components, state, context, everything. Ink lets you build interactive and beautiful CLIs in no time. Here's basic implementation of Jest's UI (view source code):

Basic Jest UI implemented with InkBasic Jest UI implemented with Ink

I hope I had your curiosity, but now I need your attention. Let's step back a bit and check out the building blocks that Ink offers, which allow building such UIs as above. Here's how the most basic Ink app looks like:

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

const Hello = () => <Text>Hello World</Text>;
render(<Hello/>);
Basic Ink AppBasic Ink App

Styling and colorizing output is essential for CLIs that want to look good. You can use Ink's built-in components such as <Text> and <Color>, which use everyone's beloved chalk library under the hood. It's as easy as this:

import {render, Color} from 'ink';

const Hello = () => <Color green>Hello World</Color>;
render(<Hello/>);
Colors colors colorsColors colors colors

Ink also implements Flexbox layout mechanism by leveraging Yoga Layout library from Facebook. You can finally forget about positioning elements in the output by using a bunch of spaces and \n. With Ink, for the first time ever you can use properties like flex, flexDirection, alignItems, justifyContent, margin, padding and width which you've only been able to use in the browser, until now. All <Box>es in Ink are flexbox-enabled (think <div style={{display: 'flex'}}/> by default). Here's an example layout to show you the power of Flexbox in CLIs:

import {render, Box, Text, Color} from 'ink';

const Flexbox = () => (
    <Box padding={2} flexDirection="column">
        <Box>
            <Box width={14}>
                <Text bold>Total tests:</Text>
            </Box>

            12
        </Box>

        <Box>
            <Box width={14}>
                <Text bold>Passed:</Text>
            </Box>

            <Color green>10</Color>
        </Box>

        <Box>
            <Box width={14}>
                <Text bold>Failed:</Text>
            </Box>

            <Color red>2</Color>
        </Box>
    </Box>
);

render(<Flexbox/>);
Flexbox in actionFlexbox in action

As I mentioned in the beginning, Ink supports all React features, such as state. If you know React, you know Ink already. In the following counter example, you can see that the only thing different here is <Text> instead of <span>:

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

const Counter = () => {
    const [count, setCount] = useState(0);

    useEffect(() => {
        const interval = setInterval(() => {
            setCount(count => count + 1);
        }, 100);

        return () => clearInterval(interval);
    }, []);

    return <Text>Count: {count}</Text>;
};

render(<Counter/>);
Hooks and stateHooks and state

React, Ink and Yoga combined provide a simple path towards creating beautiful, maintainable and testable command-line interfaces. Back in the day when we were working on AVA, I remember the fear of removing a space or a newline, which could mess up the output and fail tests. If we had Ink back then and embraced React components for generating output of our CLI, it would've been much easier to maintain and predict the consequences of the changes we were making. With Ink, you can test components individually or your app as a whole. You also don't need to think how to implement an interactive CLI output or how to properly accept input from the user. Ink took care of it for you, so you can deliver great CLIs faster than ever before.

It's really the future of writing interactive CLI tools.

Sindre Sorhus

I hope after trying out Ink you will think the same way Sindre does! Let me know on GitHub if you run into any issues or have some ideas on how to improve Ink!


On a personal note, it feels amazing to write this blog post, which concludes almost half a year of work, on and off. I want to thank Sindre for helping to shape Ink as it is today and providing extremely useful feedback and advice since day 0. Almost 2 years ago, we were only discussing what Ink could be on the remote Thai island at 1am with cocktails on the table!

I also want to thank Kat Marchán from npm team and Simen Bekkhus from Jest team for being early-adopters of Ink. Even if they don't end up using Ink in their projects, I'm still going to be happy that Ink was worth trying out! Kat's work and feedback helped stabilize Ink ahead of launch, which led to some important bug fixes and new features being introduced before the release.

Thank you! ❤️

Back to all posts