Colour levels
colorLevel() decides 0, 16, 256 or truecolor: NO_COLOR first, then FORCE_COLOR=0, the --color flags and FORCE_COLOR, then what the terminal or the CI vendor reports.
colorLevel(rt, { json }) is chalk's level — 0 none, 1 sixteen colours, 2 256,
3 truecolor — decided by the rule below, which is chalk's own apart from the places this page
names.
The rule, in order
--json— always 0. Structured output carries no escapes, even when forced.NO_COLOR— set and not empty: 0, whatever else is set.FORCE_COLOR=0orFORCE_COLOR=false— 0. An explicit "off" is settled before any flag is read, so--color=256cannot turn it back on.- The
--colorflags, when the runtime carriesargv; a flag beats anyFORCE_COLORbut 0:--no-color,--no-colors,--color=falseand--color=neverare 0;--color=16m,--color=fulland--color=truecolorare 3;--color=256is 2;--colorand--colorsturn colour on and let the terminal decide how much. Flags after--are the program's, not these. FORCE_COLOR— a number is an exact level, clamped to 3 (FORCE_COLOR=2is 2, not "2 or better");trueor empty turns colour on and lets the terminal decide.- No instruction at all: a pipe is 0, and so is
CLI_ACCESSIBLE, because escape codes are noise to a screen reader. Azure Pipelines (TF_BUILDandAGENT_NAME) is the one pipe chalk colours, and so does this. - Once colour is on, by a terminal or by an instruction:
TERM=dumbis the floor; aCIrun gets its vendor's level — GitHub Actions, Gitea Actions and CircleCI truecolor, Travis, AppVeyor, GitLab, Buildkite, Drone and Codeship 16 colours; otherwiseCOLORTERM=truecoloris 3, aTERMending in-256coloris 2, and a colour-capableTERMis 1.
import { colorLevel, outputMode } from 'roundel/policy';
const cases = [
['a terminal, TERM=xterm-256color', { env: { TERM: 'xterm-256color' }, isTTY: { stdout: true } }],
['a pipe, TERM=xterm-256color', { env: { TERM: 'xterm-256color' }, isTTY: { stdout: false } }],
['a pipe, FORCE_COLOR=true, COLORTERM=truecolor', { env: { FORCE_COLOR: 'true', COLORTERM: 'truecolor' }, isTTY: { stdout: false } }],
['a terminal, NO_COLOR=1, --color=16m', { env: { NO_COLOR: '1' }, argv: ['--color=16m'], isTTY: { stdout: true } }],
['a terminal, FORCE_COLOR=0, --color=256', { env: { FORCE_COLOR: '0' }, argv: ['--color=256'], isTTY: { stdout: true } }],
['a terminal, --no-color', { env: { COLORTERM: 'truecolor' }, argv: ['--no-color'], isTTY: { stdout: true } }],
['GitHub Actions log, FORCE_COLOR=true', { env: { CI: 'true', GITHUB_ACTIONS: 'true', FORCE_COLOR: 'true' }, isTTY: { stdout: false } }],
['a terminal, CLI_ACCESSIBLE=1', { env: { CLI_ACCESSIBLE: '1', COLORTERM: 'truecolor' }, isTTY: { stdout: true } }],
];
for (const [name, rt] of cases) console.log(`${outputMode(rt).padEnd(10)} ${colorLevel(rt)} ${name}`);
const forced = { env: { FORCE_COLOR: '3' }, isTTY: { stdout: true } };
console.log(`${outputMode(forced, { json: true }).padEnd(10)} ${colorLevel(forced, { json: true })} --json, even with FORCE_COLOR=3`);tty 2 a terminal, TERM=xterm-256color
pipe 0 a pipe, TERM=xterm-256color
pipe 3 a pipe, FORCE_COLOR=true, COLORTERM=truecolor
tty 0 a terminal, NO_COLOR=1, --color=16m
tty 0 a terminal, FORCE_COLOR=0, --color=256
tty 0 a terminal, --no-color
ci 3 GitHub Actions log, FORCE_COLOR=true
accessible 0 a terminal, CLI_ACCESSIBLE=1
json 0 --json, even with FORCE_COLOR=3Where it differs from chalk
NO_COLOR. chalk 6.0.0's colour detection does not read it; here it outranks everything.- A flag beats an ambient
FORCE_COLOR. In chalk a setFORCE_COLORoverwrites the flag's answer, so--no-coloron a machine that exportsFORCE_COLOR=3still colours. Here the flag typed for this run wins — except thatFORCE_COLOR=0is an "off", and off always wins. CLI_ACCESSIBLEand--json. chalk has neither; here each is level 0 without an explicit ask, and--jsoneven with one.- No emulator allow-list. chalk also recognises particular terminal programs —
TERM_PROGRAM, kitty, ghostty, wezterm, TeamCity and the Windows build number. This does not: a program on one of those terminals that reports nothing throughTERMorCOLORTERMasks for colour withFORCE_COLORor--color. - The level is per façade. Setting
chalk.levelonroundel/chalkchangesroundel/chalkand nothing else; the tokens keep reading the policy.
Apart from the allow-list, a differential sweep of 3,000 random environments per partition
against chalk 6.0.0's own colour detection found no divergence except where a --color flag
and FORCE_COLOR are both set, which is the second point above; policy.test.ts records it.
The output policy
outputMode() answers json, accessible, tty, ci or pipe from a runtime, first match wins — the one rule every package in the family asks before it redraws.
Tokens
Nine semantic tokens — error, warn, ok, hint, muted, command, flag, value, heading — each the identity until fly() decides a level, so a program that never flies prints plain text.