Lab color is a device-independent color space, formally CIELAB, that describes a color by lightness L and two opponent axes, a and b. The CIE created it in 1976 so that distances between colors roughly match how different they look to people. In CSS you write it with the lab() function from CSS Color Module Level 4, for example lab(54.62% 8.76 -65.79).
Lab has three coordinates.
The a and b axes are signed and have no fixed limit. In practice they stay within about plus or minus 160 for real-world colors. In CSS, a percentage maps 100% to 125 on these axes. A color with a and b near 0 is a gray, and larger a and b mean more saturated colors.
The white point is D50, a daylight spectrum of about 5000 K. sRGB uses D65, so converting from sRGB takes a chromatic adaptation step, normally the linear Bradford transform, before the Lab formula runs. The formula uses a cube root above a threshold of 216/24389, with a linear segment below it.
const mul = (M, v) => M.map(r => r[0] * v[0] + r[1] * v[1] + r[2] * v[2]);
const toLinear = v => v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
const SRGB_TO_XYZ = [[506752 / 1228815, 87881 / 245763, 12673 / 70218],
[87098 / 409605, 175762 / 245763, 12673 / 175545],
[7918 / 409605, 87881 / 737289, 1001167 / 1053270]];
const D65_TO_D50 = [[1.0479297925449969, 0.022946870601609652, -0.05019226628920524],
[0.02962780877005599, 0.9904344267538799, -0.017073799063418826],
[-0.009243040646204504, 0.015055191490298152, 0.7518742814281371]];
const WHITE = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585];
const EPS = 216 / 24389, KAPPA = 24389 / 27;
function srgbToLab(r, g, b) {
const lin = [r, g, b].map(v => toLinear(v / 255));
const xyz = mul(D65_TO_D50, mul(SRGB_TO_XYZ, lin)).map((v, i) => v / WHITE[i]);
const f = xyz.map(v => v > EPS ? Math.cbrt(v) : (KAPPA * v + 16) / 116);
return [116 * f[1] - 16, 500 * (f[0] - f[1]), 200 * (f[1] - f[2])];
}
const show = a => a.map(v => v.toFixed(2).replace('-0.00', '0.00')).join(' ');
const blue = srgbToLab(59, 130, 246);
const dark = srgbToLab(37, 99, 235);
console.log('lab(' + show(srgbToLab(255, 255, 255)) + ') white');
console.log('lab(' + show(srgbToLab(0, 0, 0)) + ') black');
console.log('lab(' + show(srgbToLab(128, 128, 128)) + ') gray 128');
console.log('lab(' + show(blue) + ') #3b82f6');
console.log('lab(' + show(dark) + ') #2563eb');
console.log('deltaE76 =', Math.hypot(...blue.map((v, i) => v - dark[i])).toFixed(2));
lab(100.00 0.00 0.00) white
lab(0.00 0.00 0.00) black
lab(53.59 0.00 0.00) gray 128
lab(54.62 8.76 -65.79) #3b82f6
lab(44.79 21.05 -75.60) #2563eb
deltaE76 = 18.54
The simplest Lab color difference is the straight-line distance between the two points, written Delta E and, in this form, known as CIE76. In the run above it is 18.54 for the two blues. Newer formulas such as Delta E 2000 correct for known weaknesses. The CSS Color 5 draft treats a Delta E 2000 of 1 or more as just visible and 5 or more as a different color.