Skip to content

Perceptual colour / restrictions

API v1

Colour that cannot
fail contrast.

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

Pick a colour. Pick a background. See what is legal.

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.

Colour schemes

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.

1 palette

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

+30°derived

Total sweep across the scale, zero at step 500. Rotation never touches lightness, so every bracket above holds at any sweep.

derived

The step where rotation is zero — the scale pivots around it.

Sweep shape

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

88of 100

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.

0across the scale

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.

Shift shape
derived

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.

Backgrounds

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.

lightdarkest: brand 50
darklightest: brand 900

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.

0
20
50
100
200
300
400
500
600
700
800
900
950
1000
UI45
400–1000, closest legal ink at Lc 52.1
Heading60
500–1000, closest legal ink at Lc 62.0
Non-body75
700–1000, closest legal ink at Lc 82.3
Large75
700–1000, closest legal ink at Lc 82.3
Body90
800–1000, closest legal ink at Lc 90.1
light

2 backgrounds · brand 50 binds — the darkest of them, so an ink legal here is legal on the rest · 800–1000 carries body text

Brand
dark

2 backgrounds · brand 900 binds — the lightest of them, so an ink legal here is legal on the rest · 0–100 carries body text

Body90
0–100, closest legal ink at Lc 90.5
Large75
0–100, closest legal ink at Lc 90.5
Non-body75
0–100, closest legal ink at Lc 90.5
Heading60
0–200, closest legal ink at Lc 73.8
UI45
0–300, closest legal ink at Lc 57.4

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

Contrast is not a preference

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.

A field, outlined at the non-text bar

dark · brand 900 · binding of 2

Contrast is not a preference

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.

A field, outlined at the non-text bar

What it is

A colour engine with an opinion about what is readable.

Most palette tools generate colours and leave contrast to you. bezold generates the colours and returns which of them you may put next to which — as enumerable sets, because the scale is twelve discrete steps and every question about it has a finite answer.

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.

Both standards, together

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.

Worst case, not best case

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.

Ranges, not picks

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.

Deterministic

The same input always produces byte-identical output. No randomness, no timestamps, nothing that makes a snapshot test a coin flip.

Hue is not a contrast lever

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

The site is the demonstration.

Every colour on this page is engine output, derived at build time from three hues measured out of the logo. There is exactly one hardcoded palette in the codebase — the logo — and it is there because it is the input, not the output.

The logo is already a bezold palette

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

Generation happens on the server

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 API this runs on →

The finding

A scale of twelve is a palette of eight.

Steps 200 through 600 carry no body text in either direction: too dark for dark ink, too light for light ink. That band is not a defect in this scale — it is in every perceptually uniform scale, and most systems meet it in review rather than in the spec. bezold states it as a bound and returns it in the response.
20
50

Lc 90 pages — dark ink

100

Gated Lc 75 page

200
300
400
500

No body text, in either direction

600

Mirrored gated page — light ink

700
800
900
950

Lc 90 pages — light ink