In the Node.js ecosystem, there are a lot of libraries for coloring text in the terminal. Every library claims things like "I'm the fastest" or "I'm the smallest".
Today, the most popular and widely used library is chΠ°lk. It's feature rich and fast - but it's not perfect.
Let's dive into this colorful world and see how libraries differ.
Unpacked size of actual libraries
The package size in node_modules
directory:
-
picocolors
: 6.37 kB (not minimized) - A micro library with basic features. -
Π°nsis
: 5.89 kB (minimized) - A powerful library with a rich set of features. -
chalk
: 44.2 kB (not minimized) - Provides similar functionality to Ansis.
Compare the features of most popular libraries
Library ______________ |
Colors support | Features | ||||
---|---|---|---|---|---|---|
- ESM | CJS - named import |
16|256|16m|π ____________ |
Fallback | Chained syntax |
Nested template strings `${}` |
LF\n
|
Supports ENV vars CLI flags |
ansis ESM CJS β
named import
|
β β β β | β256 β16 βb&w |
β | β | β |
NO_COLOR FORCE_COLOR COLORTERM --no-color --color
|
chalk v5 ESM β named import
|
β β β β | β256 β16 βb&w |
β | β | β |
NO_COLOR FORCE_COLOR --no-color --color
|
kolorist ESM CJS β
named import
|
β β β β | β256 βb&w |
β | β | β |
NO_COLOR FORCE_COLOR
|
cli-color CJS β named import
|
β β β β | β16 βb&w |
β | β | β | NO_COLOR |
colors-cli CJS β named import
|
β β β β | βb&w | β | β | β |
--no-color --color
|
colors.js CJS β named import
|
β β β β | βb&w | β | β | β |
FORCE_COLOR --no-color --color
|
ansi-colors CJS β named import
|
β β β β | β | β | β | β | FORCE_COLOR |
colorette ESM CJS β
named import
|
β β β β | βb&w | β | β | β |
NO_COLOR FORCE_COLOR --no-color --color
|
picocolors CJS β named import
|
β β β β | βb&w | β | β | β |
NO_COLOR FORCE_COLOR --no-color --color
|
tinyrainbow ESM β named import
|
β β β β | βb&w | β | β | β |
NO_COLOR FORCE_COLOR FORCE_TTY --no-color --color
|
kleur ESM CJS β
named import
|
β β β β
8 colors |
βb&w | β | β | β |
NO_COLOR FORCE_COLOR
|
Colors support
Only chalk
and ansis
support truecolor. Other libraries are limited to 16 colors, while kleur
supports only the 8 basic colors.
16
- ANSI 16 colors likered
,redBright
,bgRed
,bgRedBright
-
256
- ANSI 256 colors methods, e.g.:-
ansis
:fg(n)
,bg(n)
-
chalk
:ansi256(n)
,bgAnsi256(n)
-
cli-color
:xterm(n)
-
colors-cli
:x<n>
-
16m
- Truecolor methods, e.g.:hex()
,bgHex()
,rgb()
,bgRgb()
-
π - Colored output in Chromium-based browser console:
- β - colored output
- β - b&w output, or fatal error by compilation or in runtime
Fallback - Truecolor β 256 colors β 16 colors β no colors
The ANSI 256 colors:
If a terminal supports only 16 colors then ANSI 256 colors will be interpolated into base 16 colors.
Using the chalk
or ansis
you can colorize the text with truecolor.
import { hex, rgb } from 'ansis';
// ruby color with bold style
hex('#E0115F').bold`Ruby`;
rgb(224, 17, 95).bold`Ruby`;
import chalk from 'chalk';
// ruby color with bold style
chalk.hex('#E0115F').bold`Ruby`;
chalk.rgb(224, 17, 95).bold`Ruby`;
If you use the hex()
, rgb()
, fg()
(Ansis) or ansis256()
(Chalk) functions in a terminal not supported Truecolor or 256 colors, then colors will be interpolated.
Named import
Without a named import, you use the library name to access the colors, e.g.:
import color from 'some-lib.js';
let out = color.blue('file.js') + red(' not found!');
Using the named import, you directly import only the colors you use, e.g.:
import {red, blue} from 'some-lib.js';
let out = blue('file.js') + red(' not found!');
// OR using template literals
let out = `${blue('file.js')} ${red('not found!')}`
The colorette
support only named import, kleur
and ansis
supports both named import and normal import, others does not have a named import.
Chained syntax
Any color and style can be combined in any order.
Almost all libraries support chained syntax, except for smallest picocolors
and colorette
.
import {blue, bold} from 'some-lib.js';
let out = blue.underline('file.js') + bold.red(' not found!');
Nested template strings
In some complex cases you can use the nested template strings.
This feature is supported by ansis
only.
import { red, green, blue } from 'ansis';
let out = red`R ${green`G ${blue.bold`B`} G`} R`;
Using the chunk
it would look like:
import chalk from 'chalk';
let out = chalk.red`R` +
chalk.green`G` +
chalk.blue.bold`B` +
chalk.green`G` +
chalk.red`R`;
New lines
Not all libraries support correct style break at the end of line
, see the table above. For example, the smallest picocolors
, colorette
and kleur
cannot do this.
import { bgGreen } from 'ansis';
bgGreen`\nAnsis\nNew Line\nNext New Line\n`;
Benchmark
Setup and run
git clone https://github.com/webdiscus/ansis.git
cd ./ansis/bench
npm i
npm run bench
Tested on
MacBook Pro 16" M1 Max 64GB
macOS Monterey 12.1
Node.js v16.13.1
TerminaliTerm2
Colorette bench
c.red(`${c.bold(`${c.cyan(`${c.yellow('yellow')}cyan`)}`)}red`);
+ colorette 4,572,582 ops/sec very fast
picocolors 3,841,124 ops/sec very fast
ansis 2,669,734 ops/sec fast
kleur/colors 2,281,415 ops/sec fast
chalk 2,287,146 ops/sec fast
kleur 2,228,639 ops/sec fast
ansi-colors 1,265,615 ops/sec slow
colors.js 1,158,572 ops/sec slow
cli-color 470,320 ops/sec too slow
colors-cli 109,811 ops/sec too slow
Base colors
const colors = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'];
colors.forEach((color) => c[color]('foo'));
+ picocolors 8,265,628 ops/sec very fast
ansis 6,197,754 ops/sec fast
kleur 5,455,121 ops/sec fast
chalk 4,428,884 ops/sec fast
kleur/colors 2,074,111 ops/sec slow
colorette 1,874,506 ops/sec slow
ansi-colors 1,010,628 ops/sec slow
colors.js 640,101 ops/sec too slow
cli-color 305,690 ops/sec too slow
colors-cli 104,962 ops/sec too slow
Chained styles
colors.forEach((color) => c[color].bold.underline.italic('foo'));
+ ansis 5,515,868 ops/sec very fast
chalk 1,234,573 ops/sec fast
kleur 514,035 ops/sec slow
ansi-colors 158,921 ops/sec too slow
cli-color 144,837 ops/sec too slow
colors.js 138,219 ops/sec too slow
colors-cli 52,732 ops/sec too slow
kleur/colors (not supported)
colorette (not supported)
picocolors (not supported)
Nested calls
colors.forEach((color) => c[color](c.bold(c.underline(c.italic('foo')))));
+ picocolors 942,592 ops/sec very fast
colorette 695,350 ops/sec fast
kleur 648,195 ops/sec fast
kleur/colors 561,111 ops/sec fast
ansis 558,575 ops/sec fast
chalk 497,292 ops/sec fast
ansi-colors 260,316 ops/sec slow
colors.js 166,425 ops/sec slow
cli-color 65,561 ops/sec too slow
colors-cli 13,800 ops/sec too slow
Nested styles
c.red(
`a red ${c.white('white')} red ${c.red('red')} red ${c.cyan('cyan')} red ${c.black('black')} red ${c.red(
'red'
)} red ${c.green('green')} red ${c.red('red')} red ${c.yellow('yellow')} red ${c.blue('blue')} red ${c.red(
'red'
)} red ${c.magenta('magenta')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red(
'red'
)} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red(
'red'
)} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.green(
'green'
)} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red(
'red'
)} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.magenta(
'magenta'
)} red ${c.red('red')} red ${c.red('red')} red ${c.cyan('cyan')} red ${c.red('red')} red ${c.red(
'red'
)} red ${c.yellow('yellow')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red(
'red'
)} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} message`
);
+ picocolors 243,975 ops/sec very fast
colorette 243,139 ops/sec very fast
kleur/colors 234,132 ops/sec very fast
kleur 221,446 ops/sec very fast
ansis 211,868 ops/sec very fast
chalk 189,960 ops/sec fast
ansi-colors 121,451 ops/sec slow
colors.js 89,633 ops/sec too slow
cli-color 41,657 ops/sec too slow
colors-cli 14,264 ops/sec too slow
Deep nested styles
c.green(
`green ${c.cyan(
`cyan ${c.red(
`red ${c.yellow(
`yellow ${c.blue(
`blue ${c.magenta(`magenta ${c.underline(`underline ${c.italic(`italic`)} underline`)} magenta`)} blue`
)} yellow`
)} red`
)} cyan`
)} green`
);
+ colorette 1,131,757 ops/sec very fast
picocolors 1,002,649 ops/sec very fast
ansis 882,220 ops/sec fast
chalk 565,965 ops/sec fast
kleur/colors 478,547 ops/sec fast
kleur 464,004 ops/sec fast
colors.js 451,592 ops/sec fast
ansi-colors 362,733 ops/sec slow
cli-color 213,441 ops/sec slow
colors-cli 40,340 ops/sec too slow
HEX colors
Only two libraries supported truecolor methods: ansis
and chalk
c.hex('#FBA')('foo');
+ ansis 4,944,572 ops/sec very fast
chalk 2,891,684 ops/sec fast
colors.js (not supported)
colorette (not supported)
picocolors (not supported)
cli-color (not supported)
colors-cli (not supported)
ansi-colors (not supported)
kleur/colors (not supported)
kleur (not supported)
Conclusion
There is definitely no fastest, smallest and most functional library.
But we can identify 3 main leaders:
- picocolors smallest and fastest for simple use cases
- chalk most popular and fast for commonly use cases
- ansis small, very fast (faster than chalk) and powerful for simple and complex use cases
Don't be afraid to be modern and experiment with new things.
P.S.
The ansis is not as popular (yet) as chalk, but give a chance to a new, modern, powerful library. Just try this in your new project.
import { black, cyan, green, inverse, red } from 'ansis';
let out = green`Hello ${inverse`ANSI`} World!
${black.bgYellow`Warning:`} ${cyan`file.js`} ${red`not found!`}`;
console.log(out);
The output:
View and edit this example in browser:
Top comments (1)
That is a very comprehensive comparison! Shout out to Sindre's other color library that competes with picocolors, github.com/sindresorhus/yoctocolors