Tutorial

How to Add Color to CLI Output

Add color to CLI output with chalk so errors are red and success is green, without breaking when output is piped to a file.

On this page

  1. Introduction
  2. Use chalk for colors
  3. Detect TTY
  4. Disable on pipe
  5. Support NO_COLOR
  6. Combine with icons
  7. Test color output
  8. Common pitfalls
  9. Conclusion

Introduction

The hardest part of colorize CLI output is not writing the code — it is deciding what the code is allowed to reject. A chalk setup that accepts everything is easy to ship and expensive to operate, because every malformed input becomes a production surprise. The discipline is to enumerate the shapes the system actually receives, encode them in chalk, and let the library reject the rest loudly. This keeps the happy path fast and the failure path visible.

chalk earns its place when colorize CLI output is the bottleneck the user feels. Before that, it is overhead that slows the common case to protect a rare one. Measure the rejection rate and the tail latency, then decide whether the guard is worth its cost. A guard that never fires is a guard you can remove; a guard that fires constantly is a signal that the producer contract is wrong, not that the guard is too strict.

The sections below walk through the concrete decisions: where to put the chalk check, how to keep it fast under load, what to do when the contract changes, and how to test the failure paths so they degrade on purpose instead of by accident.

Use chalk for colors

Use chalk for colors is the first decision because it determines what the rest of the chalk pipeline can assume. A wrong choice here turns into colorize CLI output under load, and the invariant you thought chalk guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Use chalk for colors is the one most teams skip: drive chalk to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

import chalk from 'chalk';
console.log(chalk.green('done'), chalk.red('failed'));
// chalk auto-disables when not TTY

The javascript above is the shape to ship for colorize CLI output. It is small enough to audit and specific enough that the failure behavior is obvious. Adapt the constants to your traffic and latency budget, and resist the urge to handle cases the system has not seen yet.

Detect TTY

The mechanism behind Detect TTY is straightforward in chalk, but the consequences ripple. A wrong choice here turns into colorize CLI output under load, and the invariant you thought chalk guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Detect TTY is the one most teams skip: drive chalk to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

Disable on pipe

Disable on pipe is where most chalk implementations leak performance. A wrong choice here turns into colorize CLI output under load, and the invariant you thought chalk guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Disable on pipe is the one most teams skip: drive chalk to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

test('respects NO_COLOR', () => {
  process.env.NO_COLOR = '1';
  expect(chalk.red('x')).toBe('x');
});

Pin the behavior with this test. A chalk setup that passes this test degrades on purpose instead of by accident, which is the difference between a managed failure and an incident.

Support NO_COLOR

The hard part of Support NO_COLOR is not the happy path — it is what happens when chalk fails halfway. A wrong choice here turns into colorize CLI output under load, and the invariant you thought chalk guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Support NO_COLOR is the one most teams skip: drive chalk to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

Combine with icons

Combine with icons should be observable without a debugger. A wrong choice here turns into colorize CLI output under load, and the invariant you thought chalk guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Combine with icons is the one most teams skip: drive chalk to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

Test color output

Test color output is a trade, not a winner. A wrong choice here turns into colorize CLI output under load, and the invariant you thought chalk guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Test color output is the one most teams skip: drive chalk to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

Common pitfalls

The common pitfalls in colorize CLI output all reduce to one mistake: treating chalk as a formality instead of a contract. The contract is the thing the rest of the system relies on, and a weak contract propagates weakness everywhere.

  • Skipping the chalk boundary so colorize CLI output enters unvalidated.
  • Assuming chalk is fast without measuring the tail latency under load.
  • Letting colorize CLI output fail silently instead of failing loudly with a metric.
  • Testing only the happy path so the chalk failure surfaces in production.
  • Sharing mutable state across requests so one bad input poisons the next.

Conclusion

Do not conflate the conclusion with the implementation. They look similar from the outside but the failure modes differ: chalk fails fast and the caller knows, while an unchecked path fails slow and the caller guesses. Choose the one whose failure mode your system can absorb, and document the choice at the call site.

Self-check: write a test that drives chalk to the failing condition and assert the system degrades gracefully. Then colorize CLI output is a property you maintain, not a feature you hope for.

Sources: chalk, NO_COLOR

FAQ

Why chalk?
It auto-detects TTY and respects NO_COLOR. Manual ANSI codes do not.
Performance?
chalk is fast. For hot loops, build the string once.

Related Examples