Skip to content

Color Fundamentals ​

ANSI 16, 256-color, truecolor, and the escape sequences that carry them

Terminals speak color through escape sequences. Three generations of color specs coexist: ANSI 16 (1980s), 256-color indexed (1990s), and truecolor (2000s). Each adds capability without breaking the last. Understanding which terminal supports which β€” and how applications negotiate down when needed β€” is the foundation of every modern TUI.

The three color generations ​

ANSI 16 ​

The original color model from the VT100 era. Sixteen named colors β€” 8 base plus 8 bright variants β€” accessed by SGR codes 30–37 (fg) and 40–47 (bg), plus 90–97 and 100–107 for the bright variants. These are named slots, not specific RGB values. Your terminal emulator chooses what "red" actually looks like; the same ANSI "red" is crimson in Solarized Dark, peach in Gruvbox, and tomato in default xterm.

\e[31m red text \e[0m              # normal red
\e[91m bright red text \e[0m       # bright red
\e[1;31m bold+red text \e[0m       # bold changes color on some terminals

The user's color scheme maps the 16 names to actual hex values. This is what makes ANSI 16 portable: you write "red" and the user's theme decides whether that's vivid or muted. It's also what makes it unreliable if your app needs a specific shade β€” the same ANSI code renders differently everywhere.

256-color indexed ​

xterm introduced a 256-color palette in the 1990s: 16 ANSI (as above) + 216 RGB cube entries (6Γ—6Γ—6 with 51-unit steps) + 24 grayscale steps. Accessed via \e[38;5;<n>m for fg and \e[48;5;<n>m for bg.

\e[38;5;196m vivid red (cube)    \e[0m   # index 196 = #FF0000
\e[38;5;244m middle gray         \e[0m   # index 244

The RGB cube entries (16–231) map to fixed hex values β€” theme-independent. This is the cheapest way to get "specific color" without requiring truecolor. Index 16–231 formula: 16 + 36*r + 6*g + b where r,g,b ∈ 0..5. Grayscale 232–255: evenly-spaced grays from near-black to near-white.

Terminals almost universally support 256-color (it's 30+ years old). Whether an application uses it depends on $TERM (*-256color signals support) and COLORTERM.

Truecolor (24-bit) ​

The modern standard: full 16.7M colors via \e[38;2;<r>;<g>;<b>m (fg) and \e[48;2;<r>;<g>;<b>m (bg). Each channel is 0–255. No palette, no indirection β€” the terminal renders exactly the RGB you send.

\e[38;2;255;87;34m #FF5722 \e[0m

Almost every modern terminal supports truecolor (Ghostty, Kitty, iTerm2, WezTerm, Alacritty, Windows Terminal, modern xterm, GNOME Terminal). See 24-bit truecolor for the per-terminal matrix.

SGR vs OSC β€” two different escape families ​

Color delivery uses two escape-sequence families:

  • SGR (Select Graphic Rendition) β€” inline character styling. Embedded in text output. The \e[...m codes above are SGR. Applied per-cell as the terminal parses the stream.
  • OSC (Operating System Command) β€” out-of-band terminal queries and configuration. \e]10;?\a asks the terminal "what's your foreground color?"; the terminal replies with \e]10;rgb:abcd/ef12/3456\a. OSC sets the palette, not the content.

Your app emits SGR to color its output. Your terminal emits OSC responses when probed. The color scheme lives in OSC; the colored characters flow as SGR. Confusing these is a common source of bugs β€” see Terminal Detection.

The SGR attrs that aren't colors ​

SGR also carries attrs β€” bold, italic, underline, inverse, dim, strikethrough. These layer on top of color and are independent of it. Universally supported attrs:

CodeAttrNote
1boldSome terminals also brighten the color
2dimUneven support β€” alpha-blend on some, intensity-reduction on others
3italicTruly italic if the font has an italic variant; slanted otherwise
4underlineBasic single underline
7inverseSwaps fg + bg
9strikethroughNewer β€” check terminal matrix
22normal intensityTurns off bold/dim
23no italic
24no underline
27no inverse

Modern terminals add curly/dotted/dashed underlines, underline colors, and more β€” see curly underline.

Portability: writing TUI code that works everywhere ​

Applications face a choice for each colored output:

  1. ANSI 16 always β€” maximum compatibility, user's theme wins, can't pin specific shades. Old-school portability.
  2. Truecolor always β€” modern, exact colors, degrades badly on old terminals (color fallback or mojibake).
  3. Tier-based rendering β€” detect at startup, emit the best tier the terminal supports. The right answer for 2020s TUIs.

silvery and modern frameworks use approach 3: design in semantic tokens ($primary, $muted, $error), detect the terminal's tier on startup, and let the framework pick ANSI 16 / 256 / truecolor at render time. See Color Detection for the detection mechanisms.

See also ​