How to color text in the terminal
An interesting trend in the JavaScript ecosystem is the native implementation of features previously available only through third-party libraries. While some developers view this as unnecessary bloat in the Node.js runtime, these additions standardize and optimize features widely used across the ecosystem.
This post explores this trend by looking at util.styleText, a feature that allows developers to style terminal text natively without relying on external libraries like chalk.
The chalk library #
The chalk library allows developers to style terminal output with colors and text modifiers. These styles provide visual cues, making terminal output significantly more readable.
The following example demonstrates common chalk usage:
TypeScript / JavaScript
import chalk from 'chalk';
const log = console.log;
// Combine styled and normal strings
log(chalk.blue('Hello') + ' World' + chalk.red('!'));
// Chain styles using Chalk modifiers
log(chalk.blue.bgRed.bold('Hello world!'));
// Pass multiple arguments as a comma-separated list
log(chalk.blue('Hello', 'World!', 'Foo', 'bar', 'biz', 'baz'));
// Nest styles: "Hello" is red, "world" has a blue background
log(chalk.red('Hello', chalk.underline.bgBlue('world') + '!'));
// Nest styles of the same type
log(chalk.green(
'I am a green line ' +
chalk.blue.underline.bold('with a blue substring') +
' that becomes green again!'
));
The chalk library also supports template literals for multi-line strings:
TypeScript / JavaScript
log(`
CPU: ${chalk.red('90%')}
RAM: ${chalk.green('40%')}
DISK: ${chalk.yellow('70%')}
`);
In compatible terminal emulators, applications can use custom RGB and hex colors:
TypeScript / JavaScript
// Use RGB and Hex colors
console.log(chalk.rgb(123, 45, 67).underline('Underlined reddish color'));
console.log(chalk.hex('#DEADED').bold('Bold gray!'));
util.styleText #
While chalk is powerful, it adds a dependency for a purely decorative feature. Node.js introduced util.styleText in version 20.12.0 and declared it stable in version 22.x. It provides a native subset of chalk's functionality with zero dependencies.
The following table compares chalk and util.styleText.
| Feature | chalk (Library) | util.styleText (Native) |
|---|---|---|
| Syntax | Fluent / Chainable: chalk.red.bold('Text') | Functional: styleText(['red', 'bold'], 'Text') |
| Custom Colors | Yes: supports TrueColor (Hex/RGB). | No: Supports standard ANSI colors only. |
| Smart Nesting | Automatically restores parent styles. | No: Resets to terminal default. |
| Color Downgrading | Converts colors if terminal support is missing. | No: Uses only standard colors, so no conversion logic is needed. |
| Template Literals | Supports tagged templates. | No: Requires standard string interpolation. |
| Dependencies | Yes | None |
Detailed limitations #
When migrating from chalk to the native implementation, consider these functional differences.
- No chaining API: Developers cannot chain properties to combine styles. Instead, pass an array of strings as the first argument.
- Chalk:
chalk.red.bold.underline('Text') - Native:
styleText(['red', 'bold', 'underline'], 'Text')
- Chalk:
- No custom colors (Hex/RGB):
util.styleTextis strictly limited to the standard ANSI color palette (approximately 8 basic colors, 8 bright variants, and background counterparts). Chalk:chalk.hex('#DEADED')('Zombie')executes successfully.Native: Throws an error for hex/RGB values. Developers must use named colors like 'green' or 'red'.
The nesting "bleed" issue #
This is the most significant functional difference. chalk handles "un-nesting" automatically. In native Node.js, the closing tag of an inner style resets the stream to the terminal default, not the parent state.
TypeScript / JavaScript
import { styleText } from 'node:util';
console.log(
styleText('red', `Error: ${styleText('blue', 'File not found')} - check logs.`)
);
Output behavior
- "Error: " prints in red.
- "File not found": prints in blue.
- " - check logs.": prints in the default terminal color because the blue style reset the terminal state entirely.
Solution: Implementing smart nesting #
To achieve chalk-like nesting with util.styleText, developers can create a helper function that re-applies the parent style after a child style closes.
TypeScript
import { styleText } from 'node:util';
type Format = Parameters<typeof styleText>[0];
/**
* Wraps util.styleText to support smart nesting.
* It ensures that if a nested style resets formatting,
* the parent style is immediately re-applied.
*/
export function smartStyle(format: Format, text: string): string {
const placeholder = '_________';
const wrapped = styleText(format, placeholder);
const [open, close] = wrapped.split(placeholder);
// Replace the reset code with "Reset + Re-Open" to maintain parent style
const smartText = text.replaceAll(close, close + open);
return `${open}${smartText}${close}`;
}
// Native: The trailing text loses the red color
console.log(styleText('red', `Parent ${styleText('blue', 'Child')} Parent`));
// smartStyle: The trailing text remains red
console.log(smartStyle('red', `Parent ${smartStyle('blue', 'Child')} Parent`));
JavaScript
import { styleText } from 'node:util';
/**
* Wraps util.styleText to support smart nesting.
* It ensures that if a nested style resets formatting,
* the parent style is immediately re-applied.
*/
export function smartStyle(format, text) {
const placeholder = '_________';
const wrapped = styleText(format, placeholder);
const [open, close] = wrapped.split(placeholder);
// Replace the reset code with "Reset + Re-Open" to maintain parent style
const smartText = text.replaceAll(close, close + open);
return `${open}${smartText}${close}`;
}
// Native: The trailing text loses the red color
console.log(styleText('red', `Parent ${styleText('blue', 'Child')} Parent`));
// smartStyle: The trailing text remains red
console.log(smartStyle('red', `Parent ${smartStyle('blue', 'Child')} Parent`));
Reference: Supported formats #
util.styleText supports specific string values. Passing unsupported values (including capitalized variants or hex codes) throws an error.
Validation logic #
util.styleText follows a strict logic path:
- Input Check: Is the format string in the supported list? If not, throw a RangeError (ERR_INVALID_ARG_VALUE).
- Environment Check: Are colors enabled? If the NO_COLOR environment variable is set, or if stdout is not a TTY (teletypewriter), return plain text.
- Result: Return the styled string with ANSI codes applied.
flowchart TD
Start([styleText call]) --> CheckFormat{Is format in<br>Allowlist?}
CheckFormat -- No --> Error[Throw RangeError<br>ERR_INVALID_ARG_VALUE]
CheckFormat -- Yes --> CheckEnv{Colors Enabled<br>in Env?}
CheckEnv -- No --> Plain[Return Plain Text]
CheckEnv -- Yes --> ANSI[Return ANSI Styled Text]
Error --> Crash([Crash / Exception])
Plain --> Return([Return String])
ANSI --> Return
style Error fill:#f8d7da,stroke:#dc3545,color:#721c24
style Plain fill:#e2e3e5,stroke:#383d41,color:#383d41
style ANSI fill:#d4edda,stroke:#28a745,color:#155724
Supported text colors #
| Standard | Bright Variants |
|---|---|
| black | blackBright |
| red | redBright |
| green | greenBright |
| yellow | yellowBright |
| blue | blueBright |
| magenta | magentaBright |
| cyan | cyanBright |
| white | whiteBright |
Supported background colors #
| Standard | Bright Variants |
|---|---|
| bgBlack | bgBlackBright |
| bgRed | bgRedBright |
| bgGreen | bgGreenBright |
| bgYellow | bgYellowBright |
| bgBlue | bgBlueBright |
| bgMagenta | bgMagentaBright |
| bgCyan | bgCyanBright |
| bgWhite | bgWhiteBright |
Supported modifiers #
Developers can combine these with colors in the format array (e.g., ['bold', 'red']).
| Modifier | Description |
|---|---|
| bold | Bold text |
| italic | Italic text |
| underline | Underlined text |
| strikethrough | Strikethrough text |
| hidden | Hidden text |
| dim | Dim text |
| blink | Blinking text |
| inverse | Inverse colors |
| doubleunderline | Double underlined text |
| framed | Framed text |
| overlined | Overlined text |
| reset | Reset all styles |
Migration tool #
The Node.js team provides a Codemod migration tool to help transition codebases from chalk to util.styleText.
It is highly recommended to back up code before running the codemod. After the automated migration completes, developers must manually adjust edge cases, such as hex colors or complex chaining logic that the tool cannot reliably translate.