Appearance
Color Detection β
NO_COLOR, COLORTERM, OSC probes β how applications figure out what color to emit
Before emitting its first colored byte, a TUI must decide: is this terminal truecolor, 256-color, ANSI 16, or monochrome? Should I use color at all? Applications answer these with a stack of detection signals β environment variables, terminal queries, and explicit opt-outs. Getting the stack right is the difference between "looks great in every terminal" and "spews mojibake on SSH."
The detection stack β
Modern applications consult signals in this order (highest priority first):
- Explicit app flags β
--no-color,--color-tier=<tier>, app-specificSILVERY_COLOR,CLICOLOR_FORCE NO_COLOR(any value) β disable color entirely (no-color.org)TERM=dumbβ disable color entirely- Not a TTY (
!isatty(stdout)) β disable color (pipe-safe) COLORTERM=truecoloror=24bitβ enable truecolorTERM=*-directβ truecolor (less common convention)TERM=*-256colorβ 256-colorTERMcontainsxterm/screen/tmux/rxvtβ ANSI 16- Fallback β ANSI 16 (or mono if in doubt)
This is a heuristic stack. Lower tiers are safer defaults; higher tiers require explicit signals.
NO_COLOR β the universal opt-out β
The no-color.org standard: if NO_COLOR is set to any non-empty value, applications MUST NOT add color to their output. This is an accessibility + user-preference feature, not a capability question. Honor it absolutely.
sh
NO_COLOR=1 myapp # no color
NO_COLOR= myapp # empty β treated as unset (color OK)Implementation: check process.env.NO_COLOR before anything else. If set, render mono-only. Applications that attempt to "override" NO_COLOR lose user trust.
COLORTERM β the truecolor flag β
Set by terminal emulators that support 24-bit color. Values:
truecolorβ canonical24bitβ older alternative
Either value signals truecolor support. Modern terminals set it automatically. If it's unset, assume at most 256-color (the *-256color $TERM heuristic is a decent backup).
$TERM β the baseline claim β
$TERM tells you how the terminal wants to be treated, not what it actually is. See Terminal Detection for the full discussion. For color purposes, the practical rules:
$TERM pattern | Tier inferred |
|---|---|
dumb | mono |
*-direct | truecolor |
*-256color | 256-color |
*xterm*, *screen*, *tmux*, rxvt* | ANSI 16 (fallback: assume COLORTERM for truecolor) |
| empty | mono |
Most modern terminals ship $TERM=xterm-256color β the broadest compatibility setting β even when they support truecolor. COLORTERM is the truecolor signal; $TERM is the baseline floor.
OSC probing β the authoritative check β
Environment variables lie. Applications that really need to know can ask the terminal directly via OSC queries:
OSC 10/11 β foreground/background β
\e]10;?\a # query foreground
\e]11;?\a # query backgroundResponse: \e]10;rgb:abcd/ef12/3456\a (each channel is 16-bit hex in most terminals). Timing out? The terminal probably doesn't support the query β degrade gracefully. See OSC 10 foreground color queries for the support matrix.
OSC 4 β ANSI slots β
\e]4;<index>;?\a # query ANSI slot index (0β15 portable, 16β255 for 256-color)Same response shape as OSC 10/11. Slot 0β15 are the ANSI palette.
OSC 12 β cursor color β
\e]12;?\a # query cursor color (background under cursor)OSC 17/19 β selection β
\e]17;?\a # selection background
\e]19;?\a # selection foregroundLess widely supported β iTerm2, Kitty, and Terminal.app do; many others drop the query silently.
Practical probing β
- Set a short timeout (100β200ms). Terminals that don't support a query won't respond β don't block on it forever.
- Run queries in parallel when possible (fire all OSC writes, then collect responses).
- Accept partial results: missing slots fall back to formulas (e.g.,
cursorText = backgroundis universally safe). - Put the terminal into raw mode for the probe; restore the prior mode after.
@silvery/ansi ships probeColors (the OSC 4/10/11 primitive) and @silvery/theme ships detectScheme / detectTheme (the full probe + fingerprint + derive pipeline) β drop them into any TUI.
Degradation strategy β
After detection, emit at the detected tier:
- truecolor β full 16.7M via
\e[38;2;r;g;bm - 256-color β quantize hex to the 256-color cube's nearest index, emit
\e[38;5;Nm - ANSI 16 β map to named slots, emit
\e[31m/\e[91m/ etc. Let the user's theme decide what "red" looks like. - mono β strip color, rely on SGR attrs (bold, inverse, underline) for hierarchy
Your semantic token ($error) resolves to different concrete outputs depending on tier, but the token stays the same in your component code. That's the point of the abstraction.
See also β
- Color Fundamentals β ANSI 16 / 256 / truecolor escape sequences
- Color Schemes β the 22-slot user-configurable scheme
- Terminal Detection β broader detection mechanisms
- OSC color queries β per-terminal support matrix
- silvery.dev/guide/capability-tiers β silvery's detection + degradation implementation