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 |
ansisESM CJS✅ named import
|
✅ ✅ ✅ ✅ | →256 →16 →b&w |
✅ | ✅ | ✅ |
NO_COLORFORCE_COLORCOLORTERM--no-color--color
|
chalk v5ESM❌ named import
|
✅ ✅ ✅ ✅ | →256 →16 →b&w |
✅ | ❌ | ✅ |
NO_COLORFORCE_COLOR--no-color--color
|
koloristESM CJS✅ named import
|
✅ ✅ ✅ ❌ | →256 →b&w |
❌ | ❌ | ❌ |
NO_COLORFORCE_COLOR
|
cli-colorCJS❌ named import
|
✅ ✅ ❌ ❌ | →16 →b&w |
✅ | ❌ | ❌ | NO_COLOR |
colors-cliCJS❌ named import
|
✅ ✅ ❌ ❌ | →b&w | ✅ | ❌ | ✅ |
--no-color--color
|
colors.jsCJS❌ named import
|
✅ ❌ ❌ ❌ | →b&w | ✅ | ❌ | ✅ |
FORCE_COLOR--no-color--color
|
ansi-colorsCJS❌ named import
|
✅ ❌ ❌ ❌ | ❌ | ✅ | ❌ | ✅ | FORCE_COLOR |
coloretteESM CJS✅ named import
|
✅ ❌ ❌ ❌ | →b&w | ❌ | ❌ | ❌ |
NO_COLORFORCE_COLOR--no-color--color
|
picocolorsCJS❌ named import
|
✅ ❌ ❌ ❌ | →b&w | ❌ | ❌ | ❌ |
NO_COLORFORCE_COLOR--no-color--color
|
tinyrainbowESM❌ named import
|
✅ ❌ ❌ ✅ | →b&w | ❌ | ❌ | ❌ |
NO_COLORFORCE_COLORFORCE_TTY--no-color--color
|
kleurESM CJS✅ named import
|
❌ ❌ ❌ ✅8 colors |
→b&w | ✅ | ❌ | ❌ |
NO_COLORFORCE_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