One job per color
Each color has one job: identity, order, magnitude, polarity or state.
From dataviz (references/color-formula.md and references/palette.md).
Four data types, four palettes
Pick a dataset, then a palette. Tap cells to read them.
Data
Palette
Your paletteMismatch
Matched to the dataMatch
const JOB = {
identity: "categorical", // fixed slots, max 3 in a grid
magnitude: "sequential", // one hue, light → dark
polarity: "diverging", // two poles, gray zero
state: "status", // reserved hues + icon
};
Diverging: gray at zero
Change midpoint, poles and steps; watch the near-zero cells.
Midpoint
Poles
Steps
Your diverging scaleBad
Blue ↔ red, gray zeroGood
color-mix(in oklab, var(--pole) k/n, var(--mid)):root { --mid: #f0efec; --pos: #2a78d6; --neg: #e34948; }
:root[data-theme="dark"] { --mid: #383835; --pos: #3987e5; --neg: #e66767; }
function diverging(v, max = 40, n = 3) {
const s = Math.sign(v) * Math.min(n, Math.round((Math.abs(v) / max) * n));
if (s === 0) return "var(--mid)";
const pole = s > 0 ? "var(--pos)" : "var(--neg)";
return `color-mix(in oklab, ${pole} ${(Math.abs(s) / n) * 100}%, var(--mid))`;
}
Rainbow vs one hue
Which marked cell is larger? Answer in both panes.
Rainbow 0 / 0 · Blue ramp 0 / 0
Rainbow rampBad
Blue 100 → 700Good
:root {
--seq-100: #cde2fb; --seq-150: #b7d3f6; --seq-200: #9ec5f4; --seq-250: #86b6ef;
--seq-300: #6da7ec; --seq-350: #5598e7; --seq-400: #3987e5; --seq-450: #2a78d6;
--seq-500: #256abf; --seq-550: #1c5cab; --seq-600: #184f95; --seq-650: #104281;
--seq-700: #0d366b;
}
Why it matters, pros and cons
Why it matters
A palette whose structure matches the data decodes without a key. A mismatched one decodes wrong, confidently.
Pros
- A rule a review can check
- Readers learn the grammar once
- Dark mode reuses the same ramps
Cons
- Diverging needs a real zero, else use sequential
- Grids cap categorical at three hues
- Status hues need an icon and label