A Neovim colorscheme where every syntax color has the same WCAG contrast ratio against the background, and hues are evenly spaced in perceptual color space.
With the built-in Neovim package manager (vim.pack):
vim.pack.add({ "https://github.com/erietz/isocon.nvim" })
vim.cmd("colorscheme isocon")vim.cmd('colorscheme isocon')The colorscheme auto-detects vim.o.background and applies the appropriate defaults. You can also call setup() to override specific values:
require('isocon').setup({
background = '#282c34', -- any hex color; dark/light auto-detected
contrast = 5.0, -- WCAG contrast ratio for text (AA = 4.5, AAA = 7.0)
dim_contrast = 2.5, -- WCAG contrast ratio for comments / line numbers
bright_boost = 1.3, -- chroma multiplier for bright terminal colors
hues = { -- OKLCH hue angles in degrees (all optional)
red = 25,
green = 150,
yellow = 85,
blue = 260,
magenta = 305,
cyan = 200,
},
})
vim.cmd('colorscheme isocon')Defaults are chosen based on vim.o.background:
| Option | dark | light |
|---|---|---|
background |
#062329 |
#eef3ea |
contrast |
4.5 |
4.5 |
dim_contrast |
2.25 |
3.0 |
bright_boost |
1.3 |
1.2 |
red hue |
0° |
25° |
yellow hue |
60° |
85° |
green hue |
150° |
150° |
cyan hue |
180° |
200° |
blue hue |
240° |
260° |
magenta hue |
297° |
305° |
When dim_contrast is omitted it falls back to max(1.5, contrast × 0.5).
To get the 16 ANSI terminal color values for your current config (useful for configuring your terminal emulator):
require('isocon').print_colors()Or as a command:
:lua require('isocon').print_colors()Every foreground color is generated from the background using the same three-step process:
1. Target luminance: given a background luminance Y_bg and desired contrast ratio CR, solve for the foreground luminance:
- Dark background:
Y_fg = CR × (Y_bg + 0.05) − 0.05 - Light background:
Y_fg = (Y_bg + 0.05) / CR − 0.05
2. Find lightness: binary-search the Oklab L value such that the color at (L, C, H) hits Y_fg exactly.
3. Max chroma: binary-search for the highest chroma that keeps the color inside the sRGB gamut. Normal colors use 75% of that; bright variants use 100% (or normal × bright_boost, whichever is smaller).
The six hues differ by background (all overridable via hues). Light backgrounds nudge most hues to a slightly higher angle, since a hue that reads correctly at low lightness can drift at high lightness:
| Color | Dark | Light | Notes |
|---|---|---|---|
| red | 0° | 25° | Warm red; lower drifts pink, higher drifts orange |
| yellow | 60° | 85° | Pure yellow; lower is orange, higher is yellow-green |
| green | 150° | 150° | Mid green; lower is lime, higher is teal |
| cyan | 180° | 200° | Teal-cyan; lower merges with green, higher drifts blue |
| blue | 240° | 260° | Prototypical blue; lower is violet, higher is indigo |
| magenta | 297° | 305° | Clear magenta; lower is purple, higher is hot pink |
These are not evenly spaced, because colors are not evenly distributed around the perceptual wheel. Yellow and green dominate a large arc (~85°–180°, nearly a third of the wheel) while red and magenta occupy a much narrower band (~0°–40° and 300°–360°). OKLCH partially corrects for this compared to HSL, but the unevenness remains, so the hues are placed where each color is most unambiguously recognizable, not at equal intervals.
The result: every color you see in your editor has the same contrast against the background, and colors are as saturated as the gamut allows at that lightness.

