Perceptually uniform lightness
Scales are generated in HSLuv, whose L is CIE L* — so step 600 is the same perceived lightness at every hue, and a contrast assignment written for blue holds for yellow.
Perceptual colour / restrictions
API v1
Give bezold one colour. It returns a twelve-step scale whose lightness is perceptually uniform across every hue — and, for any background you pick, the exact set of values that clear WCAG and APCA on it.
Generator
The background list is restricted to surfaces that can actually carry text — a scale of twelve has eight of them, not twelve. Everything the grid shows is a floor: a step marked legal clears its rule at every hue the engine calibrates against, so an assignment you make here still holds when you change the hue.
The same eleven faces decisionsby.design offers, plus its neutral. Each one’s vibrancy sweep is derived here rather than copied.
Each colour is measured and anchored onto the board, exact hex included. Colours close enough in hue are captured into one palette — the family’s hue shift carries the scale through every one of them.
Using only your colours
Every family is anchored by your values — or the neutral stands at the single open one, which adds no colour.
Expanding your palette
Your values kept as given; open families get derived hues. The neutral never counts as expansion.
Every palette on the board carries its own sweep and level. These controls edit the one named here; the others keep whatever they were given.
Hue
Total sweep across the scale, zero at step 500. Rotation never touches lightness, so every bracket above holds at any sweep.
The step where rotation is zero — the scale pivots around it.
Linear rotates tints and shades in opposite directions; each-way rotates both the same way onto one hue. Curved swaps the straight profile for x³/x² — flat near the origin, steep at the ends.
Saturation
One axis, not three treatments. The named variants are positions on it, and at a variant’s own anchor the output is byte-identical to that variant.
The saturation analogue of the sweep: the level holds at the origin and walks toward the extremes, every step still running the full pipeline at its own level.
The step where the saturation shift is zero.
Lowers each step’s saturation to the most the press can reproduce at that step’s hue and lightness. Lightness and hue never move, so every contrast bar the screen palette holds is held here too. Applies over whatever saturation you have set.
Off by default: without it the first palette fills the neutral roles and surfaces are drawn from it. A reading that needs a neutral — a typed grey captured into a palette, or the neutral standing at a scheme position — turns it on regardless.
Pick every surface you will actually ship. The darkest light one and the lightest dark one are what bind — an ink legal on those is legal on the rest.
White is offered; pure black is not. The ladder bottoms out at step 950 (L* 5), and certifying a true-black page would mean measuring outside the published floor.
2 backgrounds · brand 50 binds — the darkest of them, so an ink legal here is legal on the rest · 800–1000 carries body text
2 backgrounds · brand 900 binds — the lightest of them, so an ink legal here is legal on the rest · 0–100 carries body text
Every bracket is a floor, not a measurement. A step inside one clears its rule at every hue the engine calibrates against, so an assignment made here holds when you change the hue — which is the whole reason the table can be published once and cached forever. Ringed swatches are the backgrounds you chose.
light · brand 50 · binding of 2
Every colour on this card was chosen by rule. The heading, the body, the edge of the field and the fill on the button each clear both standards against every background you picked.
This line is the non-body role — the quietest text these pages will legally carry.
dark · brand 900 · binding of 2
Every colour on this card was chosen by rule. The heading, the body, the edge of the field and the fill on the button each clear both standards against every background you picked.
This line is the non-body role — the quietest text these pages will legally carry.
/* Generated by bezold — https://bezold.dev
* hue 259.8, saturation 88, sweep 30° at step 500
*
* The scale is perceptually uniform: a step is the same lightness at every
* hue, so the assignments below hold if you change the hue.
*/
:root {
/* Brand */
--brand-20: #f6faff;
--brand-50: #ecf3ff;
--brand-100: #dee9ff;
--brand-200: #b8cdfd;
--brand-300: #91b0f8;
--brand-400: #5f88eb;
--brand-500: #4c6fda;
--brand-600: #4055bf;
--brand-700: #353a97;
--brand-800: #2b266e;
--brand-900: #1c1248;
--brand-950: #140833;
/* Light mode */
/* background: brand white */
/* background: brand 50 */
--page: #ecf3ff;
--bodyLc90: #2b266e;
--bodyLc75: #353a97;
--nonBody: #353a97;
--heading: #4c6fda;
--uiBackground: #5f88eb;
--fill: #4c6fda;
}
[data-theme="dark"] {
/* background: brand 900 */
/* background: brand 950 */
--page: #1c1248;
--bodyLc90: #dee9ff;
--bodyLc75: #dee9ff;
--nonBody: #dee9ff;
--heading: #b8cdfd;
--uiBackground: #91b0f8;
--fill: #b8cdfd;
}What it is
Scales are generated in HSLuv, whose L is CIE L* — so step 600 is the same perceived lightness at every hue, and a contrast assignment written for blue holds for yellow.
WCAG 2.x binds every light-mode UI-background row; APCA always binds in dark. They fail independently, so both are enforced rather than one standing in for the other.
Every legal set is the worst figure achieved across the engine’s calibration hues. Relying on a better per-palette number is how a system-wide rule becomes a per-palette accident.
The engine never returns one colour where several would have passed. Choosing among a legal set is placement, and placement belongs to whatever is consuming it.
The same input always produces byte-identical output. No randomness, no timestamps, nothing that makes a snapshot test a coin flip.
Measured: rotating hue anywhere on the wheel moves contrast by at most 0.43 Lc. That is what lets one table describe every palette.
Dogfood
Its six deep bars sit at HSLuv L 54.0–54.1. Its six light bars sit at L 67.0–67.1. One row, one lightness, six hues — the perceptual-uniformity claim drawn rather than argued. Run detectVariant over any of them and it reads vibrant.
neutral · hue 263.2
brand · hue 263.2
Which is why the controls above are chips and toggles rather than sliders. The engine never reaches a browser — the only version of the restriction that restricts anything — so each change is a request. The design compensates by returning everything for a palette in one response: the scale, both pages’ legal sets, the usage map and the CSS. Nothing calls back mid-decision.
The legal-step table is hue-independent, so it is fetched once and cached against the engine version indefinitely. A palette is twelve hexes on the wire.
The finding
Lc 90 pages — dark ink
Gated Lc 75 page
No body text, in either direction
Mirrored gated page — light ink
Lc 90 pages — light ink