Getting started
Install roundel, fly a theme once at startup, and see the same tokens in a pipe, under FORCE_COLOR, under --json, in CI and in screen-reader mode.
roundel is colour with one rule behind it. You write tokens — error, hint, command —
instead of colour names, you fly a theme once at startup with a description of the
process, and one output policy decides from that description whether anything is
coloured, and how much.
Install
npm install roundelIt is ESM with a default condition, so require('roundel/tokens') also works from CommonJS
on Node 20.19+ and 22.13+. It has no dependencies
(shape.test.ts
installs the packed tarball and checks).
The runtime
roundel never reads process itself. The policy takes a runtime — the environment, the
arguments and whether stdout is a terminal — so a test can pass a literal and a program passes
its own process. Every example on this site imports this file:
import process from 'node:process';
/** The real process as roundel reads it: the environment, the arguments and whether stdout is a terminal. */
export const rt = {
env: process.env,
argv: process.argv.slice(2),
isTTY: { stdout: process.stdout.isTTY === true },
};
/** `--json` on the command line. */
export const json = process.argv.includes('--json');argv is optional: without it the policy reads no --color flags. A CLI built on
burgee already has a runtime of this shape.
A first program
import { colorLevel, outputMode } from 'roundel/policy';
import { fly } from 'roundel/theme';
import { command, error, hint } from 'roundel/tokens';
import { json, rt } from './rt.mjs';
fly({}, rt, { json });
console.log(`mode ${outputMode(rt, { json })}, level ${colorLevel(rt, { json })}`);
console.log(JSON.stringify(`${error('missing --name')} ${hint('try')} ${command('greet --name ada')}`));JSON.stringify makes the escape codes visible. Piped, with nothing asked, nothing is
coloured — a pipe nobody asked to colour is a file or another program's input:
mode pipe, level 0
"missing --name try greet --name ada"Ask for colour and it comes, at the level asked for — the default theme's error in
truecolor, hint dimmed, command bold:
mode pipe, level 3
"\u001b[38;2;244;121;74mmissing --name\u001b[39m \u001b[2mtry\u001b[22m \u001b[1mgreet --name ada\u001b[22m"mode pipe, level 2
"\u001b[38;5;209mmissing --name\u001b[39m \u001b[2mtry\u001b[22m \u001b[1mgreet --name ada\u001b[22m"Under --json nothing is coloured even when forced, because structured output carries no
escapes:
mode json, level 0
"missing --name try greet --name ada"A CI log and a screen reader each get their own mode, and plain text:
mode ci, level 0
"missing --name try greet --name ada"mode accessible, level 0
"missing --name try greet --name ada"Every output block on this site is checked: tests/examples.test.ts writes each titled file,
runs the command in the block's title, and compares.
Two answers
| answers | used for | |
|---|---|---|
outputMode(rt, { json }) | json, accessible, ci, pipe or tty | whether to redraw — spinners, progress, prompts |
colorLevel(rt, { json }) | 0, 1, 2 or 3 | how much colour, if any |
The mode never decides the colour level, and the level never decides the mode. The output policy guide has both rules in full.
Where next
- Guides: the output policy and
interactive(), colour levels, tokens, themes and contrast, plugins. - Why roundel: what it does that chalk does not, cell by cell, with the evidence.
- Coming from chalk: change one import.
- API reference: every export of every entry point.
roundel
The colours a CLI carries. One output policy, semantic tokens, a theme, and a chalk migration path lighter than chalk. Zero dependencies.
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.