theme: ricetheme
New palette
New palette

How to make your own terminal color scheme

Build a terminal palette from scratch: a background and text, sixteen ANSI colors evened out in OKLCH and checked for contrast, then loaded and made light too.

Interface drawing of a terminal window on a blue-gray near-black, showing a git diff in soft red and green over a colored file listing, with sixteen color swatches in two rows beneath it
Sixteen colors, each with a job, all readable on one background.

tl;dr the short answer

Pick a tinted near-black background and an off-white text at 7:1 or more, then set red, green, yellow, blue, magenta and cyan at one OKLCH lightness, with the brights a step lighter. Check every color against the background for at least 4.5:1, run git and ls on it, and load it as a theme file your terminal includes.

The short version: pick a near-black background with a hint of color and an off-white text that clears 7:1 against it. Set red, green, yellow, blue, magenta and cyan at one OKLCH lightness so none of them shouts or sinks, make the brights a step lighter, check every color against the background for at least 4.5:1, then test it on real git and ls output. Hue is where your taste goes; lightness and contrast make the scheme work. Every value below comes from one worked example you can copy and recompute.

What a terminal palette has to do

A terminal scheme is two defaults and sixteen numbered colors. The default foreground and background are what plain text sits on. The sixteen ANSI colors are eight normal ones, 0 to 7 (black, red, green, yellow, blue, magenta, cyan, white), and a bright version of each, 8 to 15. Programs ask for them by number in an escape code (listed on Wikipedia’s ANSI escape code page), and your terminal decides what each number looks like.

Programs pick those numbers by meaning, so every slot has a job. With the tools’ default settings:

# Name What uses it
0 black black backgrounds and text: GNU ls draws pipes in yellow on black, /tmp in black on green
1 red removed lines in git diff, archives in ls, Starship’s prompt after a failed command
2 green added lines in git diff, executables in ls, Starship’s prompt after a success
3 yellow commit lines in git log and, in many tools, warnings
4 blue directories in ls
5 magenta images in ls, the git branch in Starship
6 cyan symlinks in ls, the @@ hunk headers in git diff, the directory in Starship
7 white “white” text, like ls’s setuid files in white on red
8 bright black zsh-autosuggestions’ suggestions (fg=8), comments in Base16 schemes
9–14 the brights emphasis, and bold text in terminals set to draw bold in the bright colors
15 bright white the brightest text

Two things follow. Color 0 is not your background: when a program asks for a black background, it gets color 0, so color 0 has to stand a little apart from the background. And editors running in truecolor, like Neovim with termguicolors on, skip these sixteen and use their own hex values, which is why changing the colorscheme in Neovim is a job of its own.

Start with the background and text

Work in OKLCH, which describes a color by lightness (L, from 0 to 1), chroma (C, how colorful it is) and hue (h, an angle). It is the polar form of Björn Ottosson’s Oklab color space, built to predict how light a color looks, and that is the property a terminal palette lives or dies on.

  1. Background: a near-black, L between 0.18 and 0.22, chroma 0.03 or less, at the hue you want the scheme to lean toward. The example uses oklch(0.20 0.015 267), which is #13161d, a blue-gray near-black.
  2. Text: the same hue with less chroma, L between 0.90 and 0.93. oklch(0.91 0.01 267) is #dee1e8, at 13.83:1 on the background.
  3. Accent: one color for the cursor and the selection. The example’s jade, oklch(0.78 0.12 175), is #52d0b3, at 9.51:1. The selection background is a dark version of it, oklch(0.32 0.04 175) or #1a3a32, and text on it still reads at 9.46:1.

Pick the eight colors, then the bright eight

Give each of the six colored slots a hue inside its family. This is the rule that keeps red a red: whatever mood the scheme has, a git diff should read at a glance.

Family OKLCH hue range Example hue
red 15–45° 25°
yellow 75–110° 90°
green 120–160° 145°
cyan 185–225° 205°
blue 235–275° 255°
magenta 290–345° 320°

Inside a range, the hue is yours: a red near 15° leans pink, one near 45° leans toward orange. Then fill in the rest:

  1. Set all six at one lightness and one chroma (the next section explains why). The example uses L 0.75 and C 0.11.
  2. Make color 0 the background with L up 0.05: #1e2229. It is quiet, but a block of it still shows.
  3. Make color 7 a light gray below the text, L 0.80: #babec6.
  4. Make color 8 a mid-gray, L 0.55: #6c717e. Readable, but clearly quieter than the other text colors.
  5. Make color 15 the text color, #dee1e8.
  6. Make each bright (9 to 14) the same hue as its normal color, with L up 0.05 and chroma down about 10%.

Even out lightness with OKLCH

The usual way to pick six colors is HSL: one saturation, one lightness, six hues. But HSL’s lightness isn’t the lightness you see. Six colors at hsl(h 70% 60%) on the example background:

Color Hex OKLCH L Ratio on background
red #e05252 0.63 4.74
yellow #e0e052 0.88 12.89
green #52e052 0.80 10.47
cyan #52e0e0 0.83 11.29
blue #5252e0 0.53 3.14
magenta #e052e0 0.68 5.56

Same “lightness”, yet yellow glares at nearly 13:1 and blue sinks to 3.14:1, below the floor. That’s the blue-on-black problem Julia Evans opens Terminal colours are tricky with, and picking by HSL walks straight into it.

Two rows of six color swatches on a near-black panel: the row labeled “HSL” has a glaring yellow and a blue that sinks to 3.14:1; the row labeled “OKLCH” reads at one even weight

Set the six at one OKLCH lightness instead, L 0.75 and C 0.11, and they land between 7.78:1 and 8.51:1: one even weight, with the hue doing the talking. Two adjustments are normal:

  • Yellow: at the same lightness as the others, yellow can read as mustard. Lift it a few hundredths if you want it brighter: at L 0.80 it becomes #d9bb66, at 9.70:1.
  • The edge of sRGB: some blues and cyans at higher chroma don’t exist as screen colors, and a picker will clip them. If a hue won’t convert cleanly, lower its chroma a little rather than its lightness.

An OKLCH color picker turns each value into the hex your terminal wants.

Check contrast against the background

WCAG 2 defines the contrast ratio as (L1 + 0.05) / (L2 + 0.05), where L1 and L2 are the relative luminances of the lighter and darker color. It runs from 1:1 to 21:1. WCAG asks for 4.5:1 for normal text and 7:1 for its stricter level. For a terminal, that gives these floors:

  • Text: 7:1 or more, and never below 4.5:1.
  • Colors 1 to 7 and 9 to 15: 4.5:1 each, since all of them print text.
  • Color 8: 3:1, under WCAG’s floor on purpose. It carries autosuggestions and comments, which should recede; lift it to 4.5:1 if you’d rather they didn’t.
  • Color 0: no floor on the background, since it’s mostly used behind text. Check text on it instead (12.19:1 here).
  • Text on the selection, and the cursor text on the accent: 4.5:1.

When a color fails, move its lightness and leave its hue alone. You can check a pair with a few lines of Python:

def luminance(hex):
    r, g, b = (int(hex[i:i + 2], 16) / 255 for i in (1, 3, 5))
    lin = lambda c: c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
    return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)

def ratio(a, b):
    hi, lo = sorted((luminance(a), luminance(b)), reverse=True)
    return (hi + 0.05) / (lo + 0.05)

print(f"{ratio('#ec928b', '#13161d'):.2f}")  # 7.80

Here is the whole example palette against #13161d:

# Name Hex Ratio # Name Hex Ratio
0 black #1e2229 1.13 8 bright black #6c717e 3.71
1 red #ec928b 7.80 9 bright red #f7a59e 9.34
2 green #80c182 8.51 10 bright green #95cf96 10.04
3 yellow #c9ab56 8.14 11 bright yellow #d6bb71 9.66
4 blue #7eb1f3 8.16 12 bright blue #92c1fd 9.71
5 magenta #cd97d9 7.78 13 bright magenta #daa9e6 9.31
6 cyan #43c2cf 8.50 14 bright cyan #66d0dc 10.02
7 white #babec6 9.71 15 bright white #dee1e8 13.83

Test it on real output

Numbers get you close; real output has the final say. Print all sixteen, as text and as backgrounds, in bash or zsh:

for i in $(seq 0 15); do
  printf '\033[38;5;%dm color %2d \033[0m \033[48;5;%dm color %2d \033[0m\n' "$i" "$i" "$i" "$i"
done

Then go through the places colors carry meaning:

  1. Run git diff in any repository. Red and green should be easy to tell apart at a glance, and the cyan @@ lines should be easy to find.
  2. Run git log and check the yellow commit lines.
  3. Run ls -l in a folder with directories, symlinks and executables, and ls -ld /tmp for black on green.
  4. Type the start of a command you’ve used before and look at the autosuggestion. It should be quiet but legible.
  5. Open a full-screen program that paints backgrounds, like htop or Midnight Commander, and check nothing vanishes into color 0.

One cost of even lightness: red and green at the same lightness are harder to tell apart with red-green color blindness. If that applies to you, separate them in lightness (a deeper red, a lighter green) and lean on the + and - in diffs.

Load it in your terminal

Keep the scheme in its own file next to your config, so swapping schemes is one line. In kitty, save this as ~/.config/kitty/mytheme.conf:

foreground           #dee1e8
background           #13161d
cursor               #52d0b3
cursor_text_color    #13161d
selection_foreground #dee1e8
selection_background #1a3a32

color0  #1e2229
color1  #ec928b
color2  #80c182
color3  #c9ab56
color4  #7eb1f3
color5  #cd97d9
color6  #43c2cf
color7  #babec6
color8  #6c717e
color9  #f7a59e
color10 #95cf96
color11 #d6bb71
color12 #92c1fd
color13 #daa9e6
color14 #66d0dc
color15 #dee1e8

Add include mytheme.conf to kitty.conf after any color lines already there, then press ctrl+shift+f5 to reload. Other terminals take the same values in their own format:

  • Alacritty: a TOML file with [colors.primary], [colors.normal] and [colors.bright] tables, loaded with import = ["~/.config/alacritty/mytheme.toml"] under [general] in alacritty.toml. The importing file loads last, so delete any [colors] tables left in it.
  • Ghostty: a file at ~/.config/ghostty/themes/mytheme with background = #13161d, foreground = #dee1e8 and one palette = 0=#1e2229 line per color, then theme = mytheme in config.ghostty (just config before Ghostty 1.2.3). ctrl+shift+, reloads it on Linux.

Choosing between them is its own question, covered in the best terminal emulator for Linux. If you’d rather start from a picture than from hue angles, the Generator builds a palette from a wallpaper by the same rules (text and the six colors at 4.5:1 or more, red kept red) and writes the theme files for kitty, Alacritty and Ghostty.

Make the light version

A light scheme isn’t the dark one inverted. Put the example’s six colors on a pale background, oklch(0.97 0.008 267) or #f3f5fb, and every one lands near 2:1. Everything has to move:

  1. Background and text: #f3f5fb and oklch(0.28 0.02 267), which is #242933, at 13.38:1.
  2. The six: L 0.50, chroma 0.10 (cyan 0.08, to stay inside sRGB). They land between 5.26:1 and 5.80:1.
  3. The brights: a step darker, not lighter, at L 0.45, since on a pale background darker is what reads as stronger.
  4. Color 0: a dark neutral at L 0.35 (#363a45), so text printed in “black” still reads.
  5. Colors 7 and 15: light grays (#cdd1d9 and #e2e4ea) that can’t carry text; programs that print white text are a known limit of every light scheme.
  6. Color 8: L 0.62, #818693, at 3.34:1.
Color Hex Ratio on #f3f5fb
red #944a46 5.79
green #3b723e 5.26
yellow #796006 5.52
blue #39659b 5.49
magenta #7c4f86 5.80
cyan #157079 5.31

Yellow pays the most: at a lightness that reads on white it turns ochre, close to olive. That’s normal, and it beats the bright yellow on white that Julia Evans calls almost impossible to read. Ghostty can follow your system’s light and dark mode with theme = light:mytheme-light,dark:mytheme.

Once the terminal is done, the same sixteen values can theme the bar, the launcher and the editor; the palettes in our Toolkit show how far one scheme can travel. To pull a palette out of an image instead, making a color scheme from your wallpaper compares pywal16, wallust and matugen, and there’s more on palettes and type in Color & Type.

Questions

What contrast ratio should terminal text have?

At least 4.5:1 against the background, which is the WCAG 2 minimum for normal text, and 7:1 or more for your main text color if you read the terminal all day. Bright black is the usual exception: it carries autosuggestions and comments, which are meant to recede, so it can sit nearer 3:1.

What are the 16 ANSI colors used for?

They are eight named colors (black, red, green, yellow, blue, magenta, cyan and white) plus a bright version of each, and programs ask for them by number rather than by hex. Tools use them for meaning: git prints removed lines in red and added ones in green, ls shows directories in blue and symlinks in cyan, and zsh-autosuggestions draws its suggestions in color 8. Your scheme decides what each number looks like.

Should a terminal background be pure black?

It doesn’t have to be. A near-black with a slight tint, around 0.18 to 0.22 lightness in OKLCH, leaves room for color 0, tab bars and status lines to sit just above it, which pure black can’t. If you want pure black anyway, make color 0 a dark gray so anything a program draws on a black background still shows.

What’s the difference between a 16-color scheme and a truecolor theme?

A 16-color scheme changes what colors 0 to 15 look like, so every program that asks for red gets your red. A truecolor theme sends exact RGB values with 24-bit escape codes and skips your palette entirely, which is what Neovim does with termguicolors on. Programs that use the 256-color palette can also reach colors outside your sixteen, so a matched setup needs the terminal scheme plus ports of it for the apps that bring their own colors.

Sources

  1. Web Content Accessibility Guidelines (WCAG) 2.2: contrast ratio and relative luminance, W3C
  2. A perceptual color space for image processing, Björn Ottosson,
  3. ANSI escape code: Colors, Wikipedia
  4. Terminal colours are tricky, Julia Evans,