loke.dev
Header image for The Terminal Palette

The Terminal Palette

Node.js finally introduced a native way to colorize console output—here is how to use it to build truly zero-dependency CLI tools.

· 4 min read

The Terminal Palette

We have spent the last decade bloating our node_modules just to turn a string green. It sounds ridiculous because it is. For years, the unofficial tax for building a pretty CLI tool in Node.js was a dependency on libraries like chalk, kleur, or picocolors. While these libraries are fantastic, the fact that we needed a third-party package to perform basic ANSI escape sequences felt like a missing piece of the runtime's soul.

That changed recently. Node.js quietly introduced util.styleText, a native way to colorize your terminal output without adding a single byte to your package.json.

The End of the "Chalk Tax"

I love chalk. We all do. But every time I started a tiny project—a quick script to automate a file rename or a simple git hook—I’d stare at the npm install command and sigh. Do I really want to manage a dependency tree just so my "Success" message isn't boring white text?

Node.js v21.7.0 (and backported to v20.12.0) finally gave us the answer: No, you don't.

The node:util module now exports a function called styleText. It’s simple, it’s fast, and it’s built-in. Here is what the basic "Hello World" of the new era looks like:

import { styleText } from 'node:util';

const errorMessage = styleText('red', 'Something went horribly wrong');
const successMessage = styleText('green', 'Task completed successfully');

console.log(errorMessage);
console.log(successMessage);

No installation. No require loops. Just clean, native code.

How it works (and why it's better)

The styleText function takes two arguments: a format (or an array of formats) and the string you want to transform. Under the hood, it’s wrapping your text in the appropriate ANSI escape codes.

What I find particularly useful is the support for arrays. You aren't limited to just a color; you can stack modifiers like bold, *italic*, or underline.

import { styleText } from 'node:util';

// Stacking styles like a pro
const warning = styleText(
  ['yellow', 'bold', 'underline'], 
  'WARNING: Your CPU is currently a baked potato'
);

console.log(warning);

Supported Styles

You get the standard hits you'd expect from any terminal library:
- Colors: red, green, blue, yellow, magenta, cyan, white, gray
- Backgrounds: bgRed, bgGreen, etc.
- Modifiers: bold, italic, underline, strikethrough, dim

Building a Zero-Dependency Logger

If you're building a CLI tool, you probably don't want to call styleText every single time you log something. You want a consistent interface. Since we’re going zero-dependency, let’s build a lightweight logger class that makes our terminal look professional without the overhead.

import { styleText } from 'node:util';

const log = {
  info: (msg) => console.log(`${styleText('blue', 'ℹ')} ${msg}`),
  success: (msg) => console.log(`${styleText('green', '✔')} ${msg}`),
  error: (msg) => console.error(`${styleText('red', '✖')} ${styleText('red', msg)}`),
  warn: (msg) => console.warn(`${styleText('yellow', '⚠')} ${msg}`),
};

log.info('Checking for updates...');
log.success('Everything is up to date.');
log.error('Failed to connect to the server.');

This tiny snippet replaces about 80% of what I used to use chalk for. It’s readable, it uses standard icons, and most importantly, it’s "future-proof" because it relies on the platform itself.

The "Is it worth it?" Factor

You might ask: "Is saving 15kb of dependencies really worth switching from a battle-tested library?"

To me, it's not about the disk space. It's about the cold start time and supply chain security. Every dependency is a potential entry point for a vulnerability and a few extra milliseconds of parsing time. When you're building a CLI tool that users run frequently, speed matters. A "zero-dep" tool feels snappy in a way that bloated tools don't.

A Quick Gotcha: Detecting Color Support

One thing chalk does automatically is check if the terminal actually supports colors (using the supports-color package). styleText is a bit more "dumb"—it will return the ANSI strings regardless of where it's running.

If you’re worried about your logs looking like a mess of [31m codes in a CI environment that doesn't support color, you should still do a quick check:

import { styleText } from 'node:util';

// A simple check for color support
const canColor = process.stdout.isTTY && process.env.TERM !== 'dumb';

function format(color, text) {
  return canColor ? styleText(color, text) : text;
}

console.log(format('blue', 'I am blue only if the terminal allows it.'));

Final Thoughts

We’re entering a "batteries-included" era for Node.js. With native test runners, native environment variable loading (--env-file), and now native text styling, the barrier to entry for high-quality tooling is dropping.

Next time you’re starting a CLI project, resist the urge to npm install chalk. See how far the native palette can take you. Your users (and your node_modules folder) will thank you.