Skip to content

Repository files navigation

colornames-oklab

4444 color names, evenly spread over the OKLab color space, covering the full Rec2020 gamut.

This project grew out of color-names, my crowd-sourced collection of 30 000+ color names. Plotting that list in perceptual color space, I observed that the names cluster heavily where humans historically looked: reds, skin tones, popular sRGB pastels — while whole regions of the gamut sit almost empty. colornames-oklab is the experiment in the other direction: what does a color name list look like when it's equally distributed? Here the positions come first — points are spread uniformly through OKLab, so every region of the gamut (including the wide-gamut colors your screen may only now be learning to show) gets a name, and no two names sit awkwardly close together.

The naming was done with Claude Fable 5 (Anthropic), point-by-point against each color's position in OKLab, using my existing color-names list as inspiration for tone and style.

Data

colornames-oklab.json — an array of 4444 entries, 374 kB raw / 90 kB gzipped:

{ "name": "Emerald", "tier": "srgb", "hex": "#089156", "oklab": [0.577, -0.1257, 0.0551] }
  • tier — smallest gamut containing the color: srgb, p3 (inside Display P3 but outside sRGB), or rec2020 (outside P3).
  • hex — sRGB fallback produced by chroma-clamping in OKLCH (hue and lightness preserved, chroma pulled into gamut). Safe everywhere.
  • oklab — the exact sampled [L, a, b] coordinates.

The schema is deliberately minimal — everything else is derivable:

  • CSS: spell the exact color as `oklab(${l} ${a} ${b})` — supported in every evergreen browser, which gamut-maps it to whatever the display can show. No stored color() strings needed.
  • OKLCH: C = Math.hypot(a, b), H = Math.atan2(b, a) * 180 / Math.PI.
import colors from 'colornames-oklab';        // ESM
const colors = require('colornames-oklab');   // CJS

Descriptions (opt-in)

Every color also has a short authored description — a separate file that adds nothing to your bundle unless you import it. It's a plain array of 4444 strings, index-aligned with the color list:

import descriptions from 'colornames-oklab/descriptions.json';

descriptions[colors.findIndex((c) => c.name === 'Eigengrau')];
// "The grey the eyes make in total darkness — intrinsic light, everyone's seen it."

Lookup API

A tiny, dependency-free closest() ships with the package. It takes raw OKLab values — convert your colors yourself (e.g. with culori) so the package stays at zero dependencies:

import { closest } from 'colornames-oklab';

// one color → the nearest named color (+ its OKLab distance)
closest([0.577, -0.126, 0.055]);
// { name: 'Emerald', tier: 'srgb', hex: '#089156', oklab: [...], distance: 0.001 }

// many colors → one match each (names may repeat)
closest([[0.7, 0.1, 0.05], [0.3, -0.1, 0]]);

// many colors, every name used at most once — like the color.pizza
// noduplicates mode: each query (in input order) takes the nearest
// name not already claimed by an earlier one
closest(palette, { unique: true });

Distance is Euclidean in OKLab — a solid perceptual metric. Results are copies; mutate them freely.

Methodology

1. Blue-noise sampling in OKLab

Points are generated with best-candidate (Mitchell) sampling in OKLab, rejection-constrained to the Rec2020 gamut: each new point is chosen from dozens of random candidates as the one farthest from all existing points. Because Euclidean distance in OKLab approximates perceptual distance, the result is a set of colors that are perceptually evenly spaced — dense nowhere, sparse nowhere.

The list was grown in five deterministic, seeded passes (1500 → 2100 → 3000 → 4000 → 4444). Each pass samples against all existing points, so extending the list never moves or renames an existing entry: ids and names are stable forever, and every future extension stays blue-noise.

2. Gamut tiers

Each point is classified by the smallest standard gamut that contains it:

tier count share meaning
srgb 2041 ~46% displayable everywhere
p3 776 ~17% needs a Display P3 screen
rec2020 1627 ~37% beyond P3 — the outer shell

These are the natural volume proportions of the gamuts in OKLab. (Surprise inside: the Rec2020-only shell is enormous, and most of it is hyper-saturated emerald, teal, cyan, and deep blue.)

3. Naming

The first 3000 names were written individually by Claude Fable 5, working through the list in hue order with each point's OKLCH coordinates and gamut tier in view, drawing on the tone and vocabulary of color-names for inspiration; the fourth and fifth passes drew the 1444 strongest unused names directly from its curated bestOf subset (via color.pizza), matched to the new points by OKLab proximity. The commonness of a name tracks its tier:

  • sRGB → the everyday canon. The colors everyone can see get the names everyone knows: Moss, Denim, Terracotta, Butter, Salmon, Charcoal.
  • P3 → vivid and recognizable. One step brighter than sRGB allows, so the vocabulary steps up too: Electric Blue, Neon Carrot, Jazzberry Jam, Shocking Pink.
  • Rec2020 → the exotic shell. Colors most screens can't show yet get rare pigment and dye names (Smaragdine, Zaffre, Eosin, Gamboge, Fuchsine), deep-sea and cosmic imagery (Benthic Teal, Event Horizon, Singularity), and physical-limit superlatives for the most extreme chroma points (Impossible Green, Maximum Fuchsia, Beyond Magenta, Greenest Green).

Two hard guarantees are enforced by scripts:

  • Uniqueness — all 4444 names are unique (validated on every build), and every hex fallback is unique too (colliding wide-gamut clamps are deterministically nudged to the nearest free hex).
  • Basics on solid ground — ~80 "obvious" names (Red, Blue, Green, Yellow, Orange, Navy, Pink, Brown, White, Black, Teal, Cyan, Gold, Peach, Turquoise, …) are audited to exist and to sit on srgb-tier points close to their reference colors. Nobody should need a P3 monitor to see "Pink".

4. Reproducibility

Everything is scripted and seeded (scripts/):

script role
sample-points.mjs initial blue-noise sampling + tier classification
sample-more.mjs progressive extension (new points vs. all existing)
make-worksheet.mjs compact per-point worksheet used during naming
merge-names.mjs merges name batches + fixes.json overrides, validates uniqueness
test.mjs / test-api.mjs data guarantees + lookup API tests (npm test)
audit-basics.mjs checks the basic-name guarantees
build-viz.mjs builds the 3D viewer

Names live in data/names/batch-*.json (keyed by stable point id) with data/names/fixes.json applied last as an override layer.

Project page & 3D viewer

docs/index.html — the project page: a standalone Three.js viewer with the full dataset embedded (orbit the OKLab solid, hover for names, filter by tier, click to copy — wide-gamut swatches render via CSS, so a P3 display shows the real thing), followed by install/usage docs. Ready for GitHub Pages (serve the docs/ folder).

npx serve .   # then open /docs/

Contributing

PRs are welcome for one thing only: better, human-invented names for existing colors. The dataset itself does not grow — positions and ids are fixed forever. See CONTRIBUTING.md.

Support

If you find this project useful, please consider buying me a coffee. It keeps my caffeine levels high enough to distinguish between #fafafa and #fbfbfb.

License

MIT

Names by Claude Fable 5 / color-name-list.

About

4444 color names, blue-noise sampled over the Rec2020 gamut in OKLab — evenly distributed perceptually, tiered by gamut (sRGB / P3 / Rec2020)

Topics

Resources

Contributing

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages