Skip to main content
ansicode

Accessibility & NO_COLOR — disabling colour, helping screen readers

Every CLI tool ships colour by default; not every user wants it, and not every environment can render it. This page covers the seven cooperating signals — NO_COLOR (the modern de-facto kill switch), CLICOLOR / CLICOLOR_FORCE (the BSD pair), FORCE_COLOR (Node's depth-aware override), TERM=dumb (the universal 'no capabilities' marker), isatty per-stream (stdout vs stderr separately), COLORFGBG (terminal background hint), and how screen readers actually read coloured output — that together let users and tools negotiate when to emit SGR bytes and when to stay silent.

NO_COLOR

NO_COLOR — the de-facto kill switch

Defined at no-color.org and adopted by ripgrep, fd, bat, eza, jq, npm, pip, cargo, gh, kubectl, docker, terraform, and most modern CLI tools. The rule is simple: if the env var NO_COLOR is set to any non-empty value, the tool MUST disable colored output. The value itself is ignored — only presence matters. NO_COLOR is the first thing your tool should check, before any TTY detection, because users with low vision, monochrome terminals, custom palettes that look broken under your colours, or screen readers explicitly opt out this way.

Either presence-only or value '1' — never check the value itself, only that the var is set.

# In bash / any POSIX shell
export NO_COLOR=1

# Disable for a single command
NO_COLOR=1 git status

# In your tool (POSIX):
if [ -n "$NO_COLOR" ]; then USE_COLOR=0; fi
CLICOLOR / CLICOLOR_FORCE

CLICOLOR & CLICOLOR_FORCE — the BSD pair

Originally from BSD (macOS ls, FreeBSD grep), now also honoured by git, ripgrep, eza, fd. Two complementary variables: CLICOLOR=0 forces colour OFF (similar to NO_COLOR but value-checked), CLICOLOR=1 (or unset) lets the tool decide based on whether stdout is a TTY, and CLICOLOR_FORCE=1 forces colour ON even when stdout is NOT a TTY (used when piping into less -R or redirecting to a file that will later be viewed in a terminal). The precedence most tools follow: NO_COLOR > CLICOLOR_FORCE > CLICOLOR > isatty(stdout).

CLICOLOR_FORCE is the right way to keep colour when piping to a pager that supports ANSI.

# Force colour through a pipe (BSD style)
CLICOLOR_FORCE=1 ls | less -R

# Disable explicitly
CLICOLOR=0 git diff
FORCE_COLOR

FORCE_COLOR — the Node.js depth convention

Adopted by chalk and the broader Node.js ecosystem (npm, yarn, mocha, jest, eslint, prettier, vite, next, tsx). Unlike NO_COLOR (binary) and CLICOLOR_FORCE (force-on), FORCE_COLOR carries an explicit depth: 0 = disabled, 1 = 16-colour (basic SGR), 2 = 256-colour (xterm-256), 3 = truecolour (24-bit RGB). FORCE_COLOR=true is equivalent to 1. The variable was added because npm's pipe-through-jq workflows lost colour by default, and the maintainers wanted a uniform override that other ecosystems could opt into. Many non-Node tools (ripgrep, fd) now also honour FORCE_COLOR even though they originated in Rust.

FORCE_COLOR encodes the desired colour depth, not just on/off.

# Force truecolour even when piped
FORCE_COLOR=3 npm test | tee build.log

# Force basic 16-colour fallback
FORCE_COLOR=1 jest --colors
TERM=dumb

TERM=dumb — the universal 'no capabilities' signal

When TERM is set to `dumb`, terminfo returns the minimal capability set: no cursor addressing, no colours, no clear-screen. Emacs uses TERM=dumb inside its shell-mode buffer because Emacs renders the output itself instead of letting the underlying terminal interpret control codes. Other environments that set it: many CI runners (GitHub Actions sets TERM=dumb by default unless you explicitly export TERM=xterm-256color), most cron jobs, some `tmux send-keys` invocations, and any process launched without a controlling terminal. A well-behaved CLI tool checks `$TERM == 'dumb'` as a colour-off signal even before NO_COLOR — emitting SGR bytes into an Emacs shell buffer or a CI log usually produces visual noise.

Pair the dumb check with the unset-TERM check — they fail in the same direction.

# Defensive check in your CLI tool
case "$TERM" in
  dumb|'') USE_COLOR=0 ;;
  *) USE_COLOR=1 ;;
esac
isatty + stderr

isatty(stdout) vs isatty(stderr) — colour each stream independently

Stdout and stderr are independent file descriptors. A command like `cmd 2>&1 | less` makes stdout a pipe (no TTY) but the stderr-detection logic on the inside hasn't changed. A defensive tool should call `isatty(STDOUT_FILENO)` to decide whether to colour stdout AND `isatty(STDERR_FILENO)` to decide whether to colour stderr — separately. The common bug: a single global `is_tty` variable computed once from stdout, then reused for stderr — causes coloured stderr to leak into log files when the user redirects 2> errors.log. NO_COLOR and CLICOLOR are read once, but TTY status is per-stream.

Two TTY checks, two booleans. One per stream you write to.

// C
int stdout_tty = isatty(STDOUT_FILENO);
int stderr_tty = isatty(STDERR_FILENO);
int err_color  = stdout_tty || stderr_tty ? 0 : (stderr_tty);
// Decide per stream — never share a single boolean.
COLORFGBG

COLORFGBG — terminal-set background-color hint

Set by many terminal emulators (urxvt originated it; konsole, terminator, gnome-terminal, mintty, xterm with the right resources, alacritty via env, and others followed). Format: `FG;BG` or `FG;decoration;BG`, where each value is the standard 16-colour index (0–15) — so `15;0` means white-on-black, `0;15` means black-on-white. The de-facto use: a tool that needs to pick a syntax theme that's readable on the user's actual background reads COLORFGBG and chooses the dark or light variant. `bat`, `mintty`, `vim`'s `:set background=` autodetect, and `neovim`'s `colorscheme` autodetect all use it. Less reliable than DECRQSS-based queries (which ask the terminal in real time), but doesn't require raw mode or a round-trip.

Index ≥ 8 in the standard palette is usually a 'bright' colour — a rough light-vs-dark heuristic.

# Crude light/dark detection in a shell script
bg="${COLORFGBG##*;}"
if [ -n "$bg" ] && [ "$bg" -ge 8 ]; then
  echo "likely light background"
else
  echo "likely dark background"
fi
Screen readers

How JAWS / NVDA / VoiceOver read escape-laden output

When a screen reader is the user's eyes, what does it do with `\x1b[31mERROR\x1b[0m`? The answer depends on whether the surrounding software pre-strips ANSI. In a terminal emulator that honours screen-reader hints (macOS Terminal + VoiceOver, Windows Terminal + Narrator), the visible 'ERROR' is announced — the ESC bytes are absorbed by the terminal's cell buffer and never reach the accessibility layer. But in less-aware terminals, or when output is read from a log file in a text editor, every byte is announced literally: 'backslash x 1 b open bracket 3 1 m E R R O R backslash x 1 b open bracket 0 m'. The takeaway: tools that emit logs intended for retroactive review should respect NO_COLOR by default to be accessible, and tools that ship coloured output should ensure at minimum that the SGR reset (`\x1b[0m`) closes every run — a stray run that continues into surrounding text quadruples the noise the screen reader produces.

See also

/strip has the regex for removing ANSI bytes after the fact; /pitfalls covers the symptom-level versions of these problems; /terminals shows which emulators honour each signal; /use lists the per-language libraries that read NO_COLOR for you.