# Category Colors > A free, browser-based optimizer for categorical color palettes: sets of colors for chart series, map categories, tags and legends that stay distinguishable from one another, including for people with color vision deficiencies. It is the reference app for the open-source `category-colors` npm package. Category Colors generates palettes with simulated annealing, maximizing the perceptual distance (CIEDE2000) between every pair of colors while respecting constraints you set: fixed brand colors, a color space and lightness or saturation range, contrast against a background, and colors to avoid. It then checks every pair under normal vision and simulated protanomaly, deuteranomaly and tritanomaly, and previews the palette on live charts. The app runs in the browser, with no account and no cost. ## When to use this - You need N colors for categorical data (chart series, map regions, labels, status tags) that people can tell apart, and N is between 2 and 20. - You need a palette that holds up under color vision deficiency, and want a pair-by-pair report of which colors get too close. - You have fixed colors (a brand palette, existing series) and need the remaining colors chosen around them. - You want to check or compare an existing palette, such as Okabe–Ito, Tableau 10 or ColorBrewer Set2, rather than make a new one. - You have a palette that mostly works and want its failing pairs repaired with small changes, keeping every color in its place. - You want the palette as CSS custom properties or W3C design tokens (DTCG JSON). It is not the right tool for sequential or diverging scales (ordered data), UI theme generation, or palettes larger than 20 colors. ## How an agent should use it - Over MCP, from any agent: the remote server at `https://categorycolors.com/mcp` (Streamable HTTP, no auth) has `generate_palette`, `fix_palette`, `check_palette`, `get_preset_palette` and `export_palette`. Every tool takes its colors as input, returns structured content with a declared output schema, and includes a `link`. `generate_palette` also takes `runs` (1–5): it runs that many times (seed, seed+1, …) and returns the lowest-cost palette. For Claude Code: `claude mcp add --transport http category-colors https://categorycolors.com/mcp`. Rate limited to 20 generations a minute per client, where each run and each fix counts, and 60 requests a minute; checks and exports cost no generations. Generations and fixes report `rateLimit.remaining`, and a refusal says how many seconds until the minute resets. - In a browser that supports WebMCP (`document.modelContext`), https://categorycolors.com/ registers these tools: `generate_palette`, `fix_palette`, `load_preset_palette`, `set_palette`, `check_palette`, `export_palette` (raw, css or json) and `get_current_palette`. Generation takes a few seconds. To call one in Chrome, get the registered tool object first; passing a name fails with "not of type 'RegisteredTool'": ```js const tool = (await document.modelContext.getTools()).find((t) => t.name === 'generate_palette') const result = await document.modelContext.executeTool(tool, { colorCount: 6, config: { jnd: 25 } }) ``` - Both `generate_palette`s take the same inputs: `config`, the full optimizer configuration (every setting the app's Configure panel has: lightness, chroma, saturation and hue windows per color space, one or several per channel as hard bounds, contrast against a background, colors to avoid, targets to pull toward, CVD weights and severities, iterations), plus shortcuts over it: `colorCount`, `style` (vivid, dusky or pastel), `fixedColors` and `jnd`. The `config` object is the JSON the Configure panel exports and imports, and the tool's input schema spells out every field. Anything the panel can set, the tool can set. - Every result echoes the settings it used as `config`, and generated palettes carry the optimizer's `cost` and the random `seed` they were made with. Runs are random; passing the same `config` and `seed` back to `generate_palette` reproduces a palette exactly, so a good run can be repeated or adjusted. The `report` lists the pairs that fall below the threshold under normal vision, protanomaly, deuteranomaly and tritanomaly, and gives `minDeltaE` per condition and overall: the closest pair's CIEDE2000 distance, a single number for comparing candidate palettes. `check_palette` takes a `jnd` of its own, and the threshold is 20 by default everywhere: the app, both MCP servers, and the npm package's CLI since 3.6.0. Given a `background` (and optionally a `ratio`, default 3), `check_palette` adds `report.contrast`: each color's WCAG contrast against it and the positions that fall short. A generated palette whose config has a contrast evaluator carries the same section. - `fix_palette` repairs a palette instead of replacing it, as the app's Fix issues does: each color keeps its position, the fewest colors that touch every failing pair move, and the rest stay exactly as they are. `strength` (0–1, default 0.35) trades pairs fixed against how far colors move. It never returns more failing pairs than it was given, and reports `issuesBefore`; call it again on its result to keep going. `pinned` takes 1-based positions that must not change, such as a brand color. The same repair is `config.behavior: "fix"` on `generate_palette`, and `config.behavior: "evaluate"` returns the `initColors` unchanged with their report. - Seeds: each `config.initColors` entry is only a starting point unless it says otherwise. `fixedColor` keeps the exact color. `lockedChannels` keeps named channels of the working space, so `["h"]` keeps the seed's hue while lightness and saturation move, the way to keep slot 2 orange. `fixedOrder` only stops the reordering step from moving that slot; it doesn't hold the color. - Wide gamut: `config.colorSpace.gamut` takes `display-p3` or `rec2020` (default `srgb`) with mode `oklch` or `oklab`. Colors past sRGB come back with `outsideSrgb: true` and `gamutCss` (such as `color(display-p3 …)`); `css` is OKLCH and exact, while `hex` is clipped. The gain is small: at 12 colors, min ΔE goes from about 23.5 to 24.0 in P3 and 24.7 in Rec. 2020. The CVD simulations and WCAG contrast are defined for sRGB, so the report notes that its numbers for those colors are extrapolations. - What to expect. These are the best of five seeds with the default evaluators, on the default okhsl space, measured 2026-10-15. Min ΔE counts normal vision and all three CVD simulations: | Colors | No constraint | 3:1 on white, as a contrast evaluator | 3:1 on white, as okhsl lightness ≤ 0.58 | |---|---|---|---| | 6 | 21 | 18 | 20 | | 8 | 19 | 15 | 17 | | 10 | 16 | 13 | 15 | | 12 | 14 | 12 | 13 | The contrast evaluator is a penalty weighed against the others, not a floor: in these runs it left colors at 2.98:1. Capping lightness in `colorSpace` holds the ratio for every color and scores better. Past these numbers, more runs rarely help; fewer colors, a lower `jnd`, or wider windows do. - In code, use the `category-colors` npm package directly: `npm install category-colors`. The app's `config` maps one-to-one onto the package's config object, and the package runs without a browser or a rate limit. - To show someone a palette, link to `https://categorycolors.com/?colors=` followed by its colors, comma-separated, each URL-encoded (any CSS notation: `?colors=%23e8a33d,oklch(0.6%200.17%20260)`). The app opens it with its charts and distinguishability report. The WebMCP tools return such a link as `link`, including the settings that made the palette. - Without either, open the app and use its Configure panel, Generate button and Output panel; it needs JavaScript. ## Docs - [category-colors on npm](https://www.npmjs.com/package/category-colors): the package the app is built on - [Source and documentation](https://github.com/ilikescience/category-colors): README, API and evaluator reference - [How to pick the least wrong colors](https://mattstromawn.com/writing/how-to-pick-the-least-wrong-colors/): the essay explaining the method ## Optional - [Matt Ström-Awn](https://x.com/mattstromawn): author