Methodology

This page exists for two situations: you are comparing scale generators and want to know what actually differs, or you have to justify a palette to a team and need something more specific than "it looked right".

Why compositing instead of HSL interpolation

The common approach is to keep hue and saturation fixed and walk lightness from near-100% down to near-0%. It is simple and it is why so many generated scales share a family resemblance.

The problem is that HSL lightness is not a perceptual quantity. At the extremes, holding S constant while L approaches 0 or 100 squeezes the achievable gamut, and the endpoints drift toward a wash of the hue rather than a recognizable dark or light version of it. You see it most on saturated oranges and yellows.

Alpha compositing has a different failure mode, and it happens to be the more useful one here. Laying the color over white or black is a linear interpolation in RGB toward a fixed point, so hue is structurally preserved — there is no mechanism by which it could drift. The endpoints are tinted white and tinted black, which is what a shade named 50 or 950 is usually meant to be.

The cost, stated plainly: perceived lightness between steps is uneven, and it is uneven differently for each hue. A yellow 400 reads much lighter than a blue 400 at the same opacity. If your system depends on shade numbers meaning the same perceived lightness across every color, this is the wrong tool and an LCH-based generator is the right one.

Semantic colors, and the check most tools skip

Given a brand color, the engine proposes six semantic roles. The interesting constraint is not contrast — it is mutual distinguishability.

Plenty of tools check contrast ratio against a background, which answers "can this text be read". Far fewer check whether your error red and your success green are distinguishable from each other for someone with red-green color vision deficiency. That is the failure that ships: a status column where every row looks identical to roughly 8% of men.

The engine runs in OKLCH, in four steps:

  1. Anchor each role to a hue family. Error lives in 340–24°, warning in 22–58°, success in 100–165°, info in 192–235°, discovery in 250–300°. Color semantics are cultural convention — leaving the family would buy perceptual distance at the price of a palette nobody reads correctly.
  2. Sample candidate hues every 3° inside that family, generating each one at the lightness and chroma the final color will actually use, so the ranking evaluates the colors that will really be produced.
  3. Score every candidate by CIEDE2000 distance against each color already reserved, including your own base color.
  4. Do that scoring under simulated protanopia, deuteranopia and tritanopia, and keep the worst of the three. A candidate is only as good as its weakest case.

The simulation uses the Machado et al. (2009) matrices at full severity, applied to linearized sRGB. CIEDE2000 is used rather than euclidean RGB distance because it approximates human perception — it catches collisions that RGB distance misses entirely.

The pass threshold is ΔE₀₀ ≥ 11, measured as the minimum across all three simulations.

Derived from #3b82f6
SuggestedProtanopiaDeuteranopiaTritanopia
success56.253.513.9
warning58.261.571.6
error15.019.566.0
discovery4.62.329.2
info16.414.09.2
neutral20.922.816.5
CIEDE2000 distance from the base under each simulation. The lowest of the three decides, and a dot marks the ones that land under 11.

What happens when two colors collide

Sometimes two roles still land too close — most often when your base color sits inside one of the semantic families, which is common for blue and green brands. The table above shows it happening: for that blue, discovery comes within ΔE₀₀ 2.3 of the base under deuteranopia, and info within 9.2 under tritanopia.

When a pair falls below the threshold, the engine does not move whichever is convenient. It compares how far each color has already strayed from its canonical hue and moves the one that has strayed further, keeping the color that is still near the middle of its family where readers expect it. The replacement is only accepted if it actually increases the distance between the two.

That resolution is where a palette stops being a lookup table. For a crimson brand color, the suggested error red has to work around the base itself; for a teal one, success and info are the pair under pressure. The suggestions you see are the output of that negotiation, which is why they differ for every input.

What this does not do

It does not check contrast ratio for you. A scale can be perfectly hue-consistent and still fail WCAG for body text at the shade you picked — that depends on the background, the text size and the weight, none of which the generator knows.

It also does not model anomalous trichromacy, the partial and far more common form of color vision deficiency. Full dichromacy is the harder constraint, so passing it is a reasonable proxy, but it is a proxy.

And it cannot tell you whether the result is right for your product. It can only tell you that the colors are far enough apart to be told apart.