cheat sheet

chalk

Package-level reference for the chalk ANSI-styling library on npm — install, ESM-only policy, peer-deps, and alternatives.

chalk

What it is

chalk is a Node.js library for applying ANSI colours, backgrounds, and text styles to terminal output via a chainable API. It is the de-facto styling layer pulled in by most Node CLIs (npm itself, eslint, jest, prettier, and thousands more) and is maintained by Sindre Sorhus.

Reach for chalk when you want the broadest API surface (256-colour, truecolor, nested chains, template strings). Reach for picocolors or kleur when you want a smaller, faster, CJS-friendly dependency without losing basic colour support.

Install

Install via your preferred package manager. The library has no CLI of its own — it is a programmatic dependency.

bash
npm install chalk

Output: added chalk to dependencies

bash
pnpm add chalk

Output: added 1 package, linked from store

bash
yarn add chalk

Output: added chalk

bash
bun add chalk

Output: installed chalk

bash
deno add npm:chalk

Output: added npm:chalk to import map

Versioning & Node support

The current major line is 5.x, which is pure ESMrequire("chalk") no longer works. Stay on 4.x (CJS/ESM dual) if your project still publishes CommonJS and cannot adopt ESM.

  • chalk@5 — Node 14+ (effectively 16+ in practice). ESM-only. No default export — import chalk from "chalk" works because the default IS the chalk instance.
  • chalk@4 — Node 10+. Dual ESM/CJS via conditional exports. Still receives security patches but no new features.
  • Semver is honoured — major bumps signal breaking changes (the 4 → 5 ESM cutover being the most recent).

Package metadata

  • Maintainer: Sindre Sorhus (sindresorhus on GitHub) and contributors
  • Project home: github.com/chalk/chalk
  • Docs: README on GitHub and npm
  • npm: npmjs.com/package/chalk
  • License: MIT
  • First released: 2013
  • Downloads: consistently in the top-5 most-downloaded npm packages — well over 200 million weekly downloads via transitive dependencies.

Peer dependencies & extras

chalk has zero runtime dependencies on the 5.x line (the supports-color and ansi-styles helpers were folded inline). No peer-deps. No extras to install.

Adjacent packages from the same maintainer that you may want alongside it:

  • chalk-template — tagged-template-literal syntax (chalk` {red {bold hello}} `). Was bundled in chalk@4; extracted in chalk@5 so the core stays minimal.
  • supports-color — detect whether the current TTY supports colour; exposed in chalk as chalk.level, but useful standalone for non-chalk output.
  • cli-color / ansi-colors — drop-in-ish alternatives; see below.

Alternatives

PackageTrade-off
picocolors~14× smaller, ~2× faster, no chaining. Used inside Node tooling (PostCSS, Vite, Prettier internals) where startup time matters. CJS + ESM.
kleurChainable API similar to chalk, smaller bundle, dual CJS/ESM. Good middle ground.
ansi-colorsFork of chalk-4 API with no dependencies and CJS support. Used by gulp/inquirer ecosystem.
coloretteTiny, no chaining, auto-detect TTY support. Drop-in for picocolors with slightly different API.
Built-in util.styleTextNode 20.12+ ships util.styleText("red", "hi"). Zero-dep but limited styles. Best for tiny scripts.

Common gotchas

  1. ESM-only since v5. const chalk = require("chalk") throws ERR_REQUIRE_ESM. Either upgrade your project to ESM ("type": "module" in package.json) or pin to chalk@^4. Many tutorials and Stack Overflow answers still reference the v4 CJS form.
  2. No default-named exports. import { red } from "chalk" does NOT work — chalk's default export is the chalk instance itself. Use import chalk from "chalk" and then chalk.red(...).
  3. chalk.level detection is unreliable in CI and Docker. Some terminals don't advertise true-colour support; check FORCE_COLOR=3 (truecolor) or FORCE_COLOR=0 (off) to override detection. CI logs sometimes strip ANSI silently, leaving raw escape codes in the build log.
  4. Piping to a non-TTY strips colour. When stdout is a pipe (mycli | less), chalk auto-disables colour. Use FORCE_COLOR=1 to force ANSI escapes through, or --color / --no-color flags wired into your CLI.
  5. Windows legacy consoles. cmd.exe and older PowerShell versions render ANSI codes as garbage. Windows Terminal and PowerShell 7+ are fine. The fix on legacy consoles is chalk.level = 0.
  6. Bundle-size in browsers. Despite the slim core, chalk is intended for Node — bundling for the browser pulls in node:tty shims. Prefer picocolors or a CSS-styled console-logger for browser code.

Real-world recipes

The companion article covers themed loggers, diffs, and banners. The recipes below target package-level concerns: capability detection, fallback hierarchies, and CLI ergonomics that work outside the controlled-demo case.

Capability-aware theme with three-tier fallback

A production CLI should adapt to four terminal tiers: truecolor, 256-colour, 16-colour, and no-colour. The pattern below picks the best representation available at module-load time and freezes it for the process lifetime.

typescript
import chalk from "chalk";

type Painter = (s: string) => string;

function pick(truecolor: Painter, ansi256: Painter, ansi16: Painter): Painter {
  switch (chalk.level) {
    case 3: return truecolor;
    case 2: return ansi256;
    case 1: return ansi16;
    default: return (s) => s;
  }
}

export const theme = {
  brand:   pick(chalk.hex("#8a5cff"), chalk.ansi256(99), chalk.magenta),
  ok:      pick(chalk.hex("#22c55e"), chalk.ansi256(40), chalk.green),
  warn:    pick(chalk.hex("#f59e0b"), chalk.ansi256(208), chalk.yellow),
  err:     pick(chalk.hex("#ef4444"), chalk.ansi256(196), chalk.red),
  muted:   chalk.dim,
};

Output: all paint functions defined; theme.brand("hi") works at any colour level.

--color / --no-color flag wiring

Most CLIs let the user override colour detection. The pattern: parse argv before any styled output, then set chalk.level accordingly.

typescript
import chalk from "chalk";

const argv = process.argv.slice(2);
if (argv.includes("--no-color") || process.env.NO_COLOR) {
  chalk.level = 0;
} else if (argv.includes("--color")) {
  chalk.level = chalk.level || 1;
} else if (argv.includes("--color=always")) {
  chalk.level = 3;
}

console.log(chalk.green("ready"));

Output: honours both env and CLI flags.

Per-instance Chalk with new Chalk()

When you need different colour levels in the same process (a CLI that writes coloured text to a TTY but plain text to a log file), construct sub-instances rather than mutating the global chalk.level.

typescript
import { Chalk } from "chalk";

const tty = new Chalk({ level: chalk.level });
const log = new Chalk({ level: 0 });

process.stdout.write(tty.green("OK") + "\n");
fs.appendFileSync("run.log", log.green("OK") + "\n");

Output: terminal shows colour; log file stays plain.

Tagged-template syntax via chalk-template

The template-tag form was extracted from chalk@4. Install separately and you regain t`{red {bold hi}} ` markup.

typescript
import t from "chalk-template";

console.log(t`Build {green succeeded} in {bold 1.2s}`);
console.log(t`{red Error} on {underline ./src/index.ts}`);

Output: styled text with nested markup parsed at runtime.

For terminals that support OSC 8 hyperlinks (iTerm2, Kitty, Wezterm, modern GNOME Terminal), pair chalk with terminal-link to produce clickable text.

typescript
import chalk from "chalk";
import terminalLink from "terminal-link";

const url = terminalLink(chalk.underline.cyan("docs"), "https://example.com/docs");
console.log(`See ${url} for details.`);

Output: clickable underlined cyan "docs" link; falls back to plain URL on unsupported terminals.

Testing & CI integration

CLI output assertions are the bulk of CLI testing. Two patterns dominate: snapshot tests against the styled output, and assertion against the stripped output.

Strip-ANSI before asserting

The most stable approach — your tests assert on content, not codes, and remain green even when chalk's encoding changes minor.

typescript
import { describe, it, expect, vi } from "vitest";
import stripAnsi from "strip-ansi";
import chalk from "chalk";

describe("logger", () => {
  it("prefixes warnings", () => {
    const log = vi.spyOn(console, "log").mockImplementation(() => {});
    console.log(chalk.yellow("WARN") + " cache miss");
    const written = stripAnsi((log.mock.calls[0] as string[])[0]);
    expect(written).toBe("WARN cache miss");
  });
});

Output: 1 passed.

Force colour in CI for log readability

GitHub Actions, GitLab CI, and CircleCI all support ANSI in log viewers. Force colour explicitly because chalk auto-detects CI=true and disables.

yaml
# .github/workflows/test.yml
env:
  FORCE_COLOR: 2
  CI: true

Output: test runner output appears coloured in the GitHub Actions log viewer.

Snapshot tests against styled output

Cleaner-looking but flakier — chalk version bumps or terminal-capability changes break snapshots. Pin chalk.level deterministically before snapshotting.

typescript
import { test, expect } from "vitest";
import chalk from "chalk";

test("banner", () => {
  chalk.level = 3;
  const out = chalk.hex("#8a5cff").bold("HELLO");
  expect(out).toMatchInlineSnapshot(`"[1m[38;2;138;92;255mHELLO[39m[22m"`);
});

Output: inline snapshot matches; regenerate with vitest -u after a chalk upgrade.

Version migration guide

The 4 → 5 jump is the largest break in chalk's history and the most common upgrade blocker. Plan it as a project-wide ESM migration, not a single dependency bump.

FromToRequired change
chalk@2chalk@4Drop the explicit new Chalk.constructor() instantiation; the default export already is the instance.
chalk@3chalk@4Replace chalk.default with chalk (the default-as-namespace shim was removed).
chalk@4chalk@5Convert package to ESM ("type": "module"), replace all require("chalk") with import chalk from "chalk".
chalk@4chalk@5If template literals (chalk`…`) were used, install the extracted chalk-template package — the built-in tag was removed.
chalk@4chalk@5Remove direct imports of supports-color; chalk re-exports it.

ESM conversion checklist:

  1. Add "type": "module" to package.json.
  2. Rename .js source files using module.exports to .cjs if they need to stay CJS — otherwise convert to import / export.
  3. Add .js extensions to relative imports (./utils./utils.js). This is required by Node's ESM resolver.
  4. Update Jest / ts-jest / Mocha config for ESM (or switch to Vitest, which is ESM-native).
  5. Replace any CJS-only deps that don't expose ESM with their dual-published replacements.

If a wholesale ESM migration is impossible, the dynamic-import escape hatch keeps you on a CJS codebase while still consuming chalk@5:

javascript
// CJS file
async function withColor() {
  const { default: chalk } = await import("chalk");
  console.log(chalk.red("ok"));
}

This is a per-call workaround. It works because dynamic import() is allowed in CJS, while top-level require() is not.

ESM/CJS interop & bundling

Chalk@5 is pure ESM ("type": "module" + "exports": { ".": { "import": "./source/index.js" } }) with no require condition. The bundler implications are concrete:

Bundler / runtimeBehaviour
esbuild (Vite, tsup)Handles ESM-only deps natively. No extra config needed.
webpack 5Works if target: "node" and experiments.outputModule = true or output is ESM. Older configs may surface "Cannot use import statement outside a module".
RollupNative ESM — no shim required.
tscRequires "module": "Node16" (or higher) plus "moduleResolution": "node16" so the compiler honours the "exports" map.
ts-node (legacy)Needs --esm flag or a tsconfig.json with "module": "esnext". Many projects move to tsx instead.
Node 16+Works as long as the calling package is ESM ("type": "module" or .mjs).
BunWorks natively in ESM and CJS — Bun resolves the "import" condition automatically.
Deno (via npm:chalk)Works; Deno only consumes ESM.
Cloudflare WorkersWorks — Workers are ESM-only since the Modules format. Bundle size matters: chalk is small enough not to be a concern.

The "exports" map in chalk's package.json blocks deep imports like require("chalk/source/utils") — you can only import the public surface. This was deliberate to prevent users from depending on internals.

Plugin & ecosystem coverage

Chalk's tiny core is intentionally complemented by sibling packages from the same maintainer (Sindre Sorhus). They share a consistent design language and are commonly installed together.

PackageRole
chalk-templateTagged-template-literal syntax extracted from chalk@4 (t` {red {bold hi}} `). Install when you need terse markup in long strings.
chalk-animationAnimated gradients (rainbow, pulse, glitch, radar, neon, karaoke). Useful for banners and easter eggs.
supports-colorTTY capability detection. Re-exported by chalk; useful standalone for non-chalk output paths.
ansi-escapesCursor moves, screen clears, hyperlinks, image rendering (iTerm2 / Kitty). Pairs with chalk for rich TUIs.
ansi-stylesThe raw style tables chalk is built on (chalkansi-styles + a chainable wrapper).
strip-ansiRemove escape codes from a string before measuring width or writing to a log file.
wrap-ansiWord-wrap text without breaking inside escape sequences.
cli-truncateTruncate a styled string to a column width.
oraSpinner library; uses chalk for colour by default.
boxenBox-drawn frames around chalk-styled content.
gradient-stringMulti-stop linear gradients across a string's characters.
figletASCII-art text rendering; pair with chalk.hex(...) for branded banners.

These layer cleanly because they all respect chalk.level and NO_COLOR. Picking a different colour library breaks the chain — most expect chalk specifically.

Security considerations

Chalk itself is low-risk — it only emits escape sequences and has zero runtime dependencies on the 5.x line. The risks live around it.

  • Untrusted strings in styled output. If user input is passed through chalk.red(input) and the input contains its own ANSI escape codes, the result can move the cursor, clear the screen, or even (with some terminals) execute commands via OSC 8 / OSC 52 sequences. Strip with strip-ansi before styling: chalk.red(stripAnsi(input)).
  • Log injection. Coloured logs forwarded to a SIEM or aggregator may include attacker-controlled newlines that fake additional log lines. Always log JSON / NDJSON in production and reserve chalk for human-facing TTYs.
  • Dependency-tree audit. chalk@5 has zero deps, but transitive imports of older chalk@2/@3 show up in many lockfiles via deeply nested packages. Run npm ls chalk to see the full tree; deduplicate with npm dedupe or pnpm's hoist settings.
  • supports-color env variables. FORCE_COLOR=2 set globally in a shared CI runner forces every chalk consumer to emit codes — useful for build logs, but corrupts any tooling that parses stdout. Set per-job, not user-wide.
  • OSC 8 hyperlinks in untrusted output. terminal-link produces clickable URLs. Never wrap user-supplied URLs without validating the protocol — terminal-link("click", javascript:...) would render a clickable malicious link in some terminals.

Troubleshooting common errors

Error [ERR_REQUIRE_ESM]: require() of ES Module chalk — chalk@5 is ESM-only and your file is CJS. Either set "type": "module" in package.json, rename the file to .mjs, or downgrade to chalk@4.

SyntaxError: Cannot use import statement outside a module — your file uses import but package.json lacks "type": "module" and the file isn't .mjs. Add the type field or rename.

TypeError: chalk.red is not a function — you imported destructured (import { red } from "chalk"); chalk has no named exports. Use the default: import chalk from "chalk".

Output shows raw escape codes like [31mhi[39m — the terminal isn't ANSI-aware. On Windows, switch to Windows Terminal or set chalk.level = 0. On CI, the log viewer may be stripping ANSI; set FORCE_COLOR=2 and view in a real terminal.

No colour despite a colour-capable terminalNO_COLOR=1 is set in the environment, output is being piped (| tee), or CI=true is auto-detected. Set FORCE_COLOR=1 (or 2 / 3) to override.

Colours look wrong in tmux / screen — these multiplexers strip the truecolour escape sequence by default. Set set -g default-terminal "tmux-256color" (or xterm-256color) and re-launch the session.

chalk.level is 0 in tests — Vitest and Jest run with a piped stdout, so chalk detects no TTY. Force colour via FORCE_COLOR=3 in the test command, or assert on the plain string with strip-ansi.

chalk.hex(...) outputs the nearest 16-colour approximationchalk.level < 3. Override with chalk.level = 3 only if you're certain the terminal supports it.

Hyperlinks don't render — chalk doesn't ship a link() method. Use terminal-link from the same author family — it falls back to plain text on unsupported terminals.

Performance tuning

Chalk is fast for any realistic CLI workload but optimisations matter when wrapping millions of strings (log replay, large file processors).

  • Cache style chains. chalk.red.bold.bgWhite walks the builder chain every call. Hoist to a constant: const errTag = chalk.red.bold.bgWhite; then errTag(text).
  • chalk.level = 0 is a fast-path. When colour is off, chalk skips wrapping and returns the bare string. The check is if (this.level <= 0) return string; near the top of the builder, so disabling colour costs nothing.
  • Avoid chalk.hex(...) on a hot path. Hex parsing happens per call. Precompute a painter: const brand = chalk.hex("#8a5cff"); brand(text);.
  • Skip styling for large strings destined for a file. Writing styled output to a logfile balloons disk usage and slows reads. Branch on process.stdout.isTTY and skip chalk when writing to a non-TTY.
  • Picocolors is ~2× faster on a per-call benchmark. If you wrap >10⁶ strings per second, switch — but most CLIs are I/O-bound long before chalk becomes the bottleneck.

A practical micro-benchmark:

typescript
import chalk from "chalk";

const red = chalk.red;
const N = 1_000_000;

console.time("hoisted");
for (let i = 0; i < N; i++) red("hi");
console.timeEnd("hoisted");

console.time("inline");
for (let i = 0; i < N; i++) chalk.red("hi");
console.timeEnd("inline");

Output: hoisted ~40ms, inline ~120ms on Node 22 — the chain walk is the cost.

When NOT to use this

Skip chalk when:

  • The output is machine-consumed (JSON for jq, NDJSON logs ingested by Loki, CSV piped to a spreadsheet). ANSI sequences corrupt the parse — keep stdout clean and reserve colour for stderr if needed.
  • The audience is non-terminal — log aggregators, CI annotation systems (GitHub Actions, GitLab), and email reports strip or mangle escape codes. Use the platform's native annotation format (::warning:: for Actions, [1;31m for nothing).
  • Bundle size dominates — a CLI library imported by other libraries should prefer picocolors (~0.4 kB) to avoid imposing chalk on every transitive consumer.
  • Bash already does it — for tiny scripts where printf '\033[31m%s\033[0m\n' hi does the job, the dependency isn't worth the install.
  • The terminal target is uncertain — for cross-platform tools that may run in cmd.exe, antique consoles, or stripped Docker base images, plain text + glyphs ( / / ) communicate without ANSI risk.
  • Browsersconsole.log("%c styled", "color: red") uses the DevTools styling API; chalk's ANSI escape codes don't render in browser dev tools.

See also