util.styleText()
Wraps text in ANSI colour codes, returning it unchanged when the target stream cannot show colour.
util.styleText(format, text[, options])- Added in Node.js v21.7.0, v20.12.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$format | string | Array | A text format, or an array of formats, drawn from util.inspect.colors — or a hex colour in #RGB or #RRGGBB form. |
$text | string | The text to be formatted. |
$options.validateStreamoptional | boolean= true | When true, the stream is checked to see whether it can handle colours before any codes are emitted. |
$options.streamoptional | Stream= process.stdout | The stream that is validated for colour support. |
Return value
stringThe text wrapped in the requested ANSI escape sequences, or the text unchanged when the stream should not be coloured.
Errors
Throws ERR_INVALID_ARG_VALUE if `format` is not a known style name, and ERR_INVALID_ARG_TYPE if `text` is not a string.
Changelog
- PHP v26.1.0, v24.16.0Hexadecimal colours such as '#ff8800' are accepted as a format.
- PHP v24.2.0, v22.17.0The 'none' format was added as a no-op, useful for conditional styling.
- PHP v23.5.0, v22.13.0styleText became stable.
- PHP v22.8.0, v20.18.0Respects stream.isTTY and the NO_COLOR, NODE_DISABLE_COLORS and FORCE_COLOR environment variables. Before this, codes were always emitted.
util.styleText() is Node's built-in answer to chalk and picocolors: a dependency-free way to colour terminal output. It takes a style name (or an array of them), the text, and returns the styled string.
The behaviour that surprises people
Since v22.8.0 / v20.18.0 it is stream aware. Before emitting anything it checks whether options.stream (default process.stdout) is a TTY and honours NO_COLOR, NODE_DISABLE_COLORS and FORCE_COLOR. Pipe your program into less, redirect it to a file, or run it in a CI job, and styleText() quietly returns the plain text — which is exactly what you want in a log file, and exactly what looks like a bug when you assert on the styled value in a test.
When to reach for it
Use it in CLIs and build tooling for warnings, errors and diffs. Two rules keep it painless: when you write to stderr, pass { stream: process.stderr } so the right stream is checked; and when you genuinely want the codes regardless — writing to a pty, or generating a fixture — pass { validateStream: false }. Unknown style names throw rather than being ignored.
Example
When to use it
- Highlight errors and warnings in a CLI without adding a colour dependency.
- Emit a coloured diff or test summary that degrades to plain text in CI logs.
- Underline file paths in tool output so they stay clickable and readable in a terminal.
Frequently asked questions
Why does util.styleText return plain text with no colour?
Because the target stream is not a TTY — output is being piped, redirected to a file, or captured by a test runner or CI — or `NO_COLOR`/`NODE_DISABLE_COLORS` is set. Pass `{ validateStream: false }` to emit the codes anyway, or set `FORCE_COLOR=1` in the environment.
Can I use util.styleText instead of chalk?
Yes, for the common cases. It covers the standard 16 colours plus their bright variants, backgrounds and modifiers such as bold, dim and underline, composes them via an array, and does TTY and NO_COLOR detection for you — with no dependency. Chalk still wins on nested template composition and older Node support.
How do I colour output written to stderr?
Pass the stream explicitly: `util.styleText('red', msg, { stream: process.stderr })`. The default is `process.stdout`, so without this the wrong descriptor is checked and colour can be dropped whenever stdout alone is redirected.
More examples
Styling stderr correctly
`options.stream` defaults to `process.stdout`, so styling destined for stderr is validated against the wrong file descriptor. In the common shell case `node app.js > out.log` — stdout redirected, stderr still on the terminal — the default check disables colour for a message the user would have seen in colour. Passing `{ stream: stderr }` makes each decision match its destination.
const util = require('node:util');
const { stdout, stderr } = require('node:process');
// Wrong: stdout is checked even though the text goes to stderr.
// Redirecting only stdout to a file silently disables the colour here.
console.error(util.styleText('red', 'Error! Error!'));
// Right: validate the stream the text is actually written to.
console.error(util.styleText('red', 'Error! Error!', { stream: stderr }));
// Small helpers for a CLI.
const warn = (msg) => util.styleText(['bold', 'yellow'], msg, { stream: stderr });
const ok = (msg) => util.styleText('green', msg, { stream: stdout });
console.log(ok('build succeeded'));
console.error(warn('2 deprecation warnings'));Composing styles, hex colours, and opting out
Styles come from `util.inspect.colors`, so that object is the authoritative list of valid names — modifiers such as `bold` and `dim`, sixteen foreground colours plus their `Bright` variants, and the `bg` backgrounds. An array nests the codes, letting you combine a modifier, a background and a foreground in one call. Note the failure mode: a misspelled style is an `ERR_INVALID_ARG_VALUE` throw, not silently ignored, so validate any style name that comes from user configuration.
const util = require('node:util');
const opts = { validateStream: false }; // show the codes regardless of stream
// A single style, or an array applied outside-in.
console.log(util.styleText('underline', 'link', opts));
console.log(util.styleText(['bold', 'bgRed', 'white'], ' FAIL ', opts));
// 'none' is a no-op, which makes conditional styling branch-free.
const emphasis = process.env.PLAIN ? 'none' : 'bold';
console.log(util.styleText(emphasis, 'maybe bold', opts));
// Hex colours (Node v26.1.0 / v24.16.0 and later).
// console.log(util.styleText('#ff8800', 'orange', opts));
// The full list of names lives here.
console.log(Object.keys(util.inspect.colors).slice(0, 6));
// [ 'reset', 'bold', 'dim', 'italic', 'underline', 'blink' ]
// Unknown names throw rather than degrading.
try {
util.styleText('mauve', 'x');
} catch (err) {
console.log(err.code); // ERR_INVALID_ARG_VALUE
}How the environment overrides the TTY check
The shell comments show the four cases a CLI actually hits. The point of the example is that `report()` contains no `isTTY` branch of its own — the detection that colour libraries traditionally make you write is built into `styleText()`. `FORCE_COLOR=1` is the escape hatch when you are piping into a pager or a CI log viewer that does understand ANSI.
// styleText consults these before emitting any escape codes.
//
// NO_COLOR=1 -> never colour (https://no-color.org)
// NODE_DISABLE_COLORS=1 -> never colour
// FORCE_COLOR=1 -> always colour, even into a pipe
//
// $ node cli.js # TTY -> coloured
// $ node cli.js | cat # pipe -> plain
// $ FORCE_COLOR=1 node cli.js | cat # pipe -> coloured
// $ NO_COLOR=1 node cli.js # TTY -> plain
const util = require('node:util');
function report(line) {
// No manual isTTY check needed: styleText already did it.
console.log(util.styleText('cyan', line));
}
report('resolved 412 packages');
// If you must know what will happen before calling:
const willColor = Boolean(process.stdout.isTTY) &&
!process.env.NO_COLOR &&
!process.env.NODE_DISABLE_COLORS;
console.log('colour enabled:', willColor || Boolean(process.env.FORCE_COLOR));
Discussion