wandres.dev
COLOR II · OKLCH y los espacios perceptuales

Lab, LCH, Oklab y OKLCH: quién es quién

Cómo se relacionan los cuatro espacios perceptuales que CSS entiende, por qué sus números no son intercambiables, y la demostración numérica del defecto azul de CIELAB.

⏱ 18 min

CSS entiende cuatro funciones de color perceptual: lab(), lch(), oklab() y oklch(). Los nombres se parecen tanto que invitan a tratarlas como sinónimos con distinta precisión, y no lo son: usan puntos blancos distintos, escalas distintas y modelos distintos. Saber en qué se diferencian no es erudición, porque hay un caso —el azul— donde la elección cambia el resultado de forma que se ve a un metro de la pantalla.

🎯 Al terminar esta lección sabrás
  • Situar XYZ como sistema de referencia del que derivan todos los demás.
  • Explicar por qué el mismo color tiene números distintos en lch() y en oklch().
  • Demostrar el desvío de tono de CIELAB en los azules.
  • Elegir con criterio cuál de las cuatro funciones usar en cada caso.

XYZ, el sistema de referencia

Debajo de todos los espacios de color hay uno solo: CIE XYZ, definido en 1931 a partir de experimentos de igualación de color con observadores humanos. Sus tres coordenadas no corresponden a ningún color real ni a ningún primario físico; son una base elegida para que la segunda coordenada, Y, coincida exactamente con la luminancia, y para que las tres sean siempre positivas.

Todo lo demás es una transformación de XYZ. sRGB es una matriz de 3x3 más una función de transferencia. Display P3 es otra matriz con la misma función. CIELAB es una transformación no lineal. Oklab es otra. Cuando escribes color(xyz 0.2 0.3 0.4) estás hablando directamente en el sistema de referencia, cosa que casi nunca querrás hacer a mano pero que existe y es útil como paso intermedio.

Un espacio XYZ necesita además un punto blanco: qué coordenadas se consideran “blanco” en las condiciones de observación. D65 aproxima la luz del día a 6500 K y es el punto blanco de sRGB, de Display P3 y de la mayoría de las pantallas. D50 aproxima 5000 K y es el estándar de las artes gráficas, porque es la temperatura de las cabinas de visualización de imprenta.

Aquí llega la primera diferencia real entre las cuatro funciones de CSS: lab() y lch() usan D50; oklab() y oklch() usan D65. Cuando el motor convierte entre unos y otros aplica una adaptación cromática de Bradford, que es la que especifica CSS Color 4. No es un detalle cosmético: es una de las razones por las que los números no se parecen.

Los mismos colores, números distintos

Compara el azul primario de sRGB en las dos familias:

.a { color: lab(29.57% 68.29 -112.03); }
.b { color: lch(29.57% 131.2 301.36); }
.c { color: oklab(45.2% -0.032 -0.312); }
.d { color: oklch(45.2% 0.313 264.05); }

Son el mismo color, #0000ff, escrito de cuatro maneras. La claridad pasa de 29.57 a 45.2 y el tono de 301 a 264. Los grados de tono de LCH y de OKLCH no son comparables: cada familia coloca sus ejes de oponencia de forma distinta, así que el mismo color cae en ángulos distintos.

El gris medio deja el contraste todavía más claro:

Color lab() oklch()
#808080 lab(53.59% 0 0) oklch(59.99% 0 0)

53.59 frente a 59.99. Si copias una escala de claridad diseñada para CIELAB y la pegas en oklch(), te sale más clara de lo que esperabas en toda la parte media. Es un error frecuente al portar sistemas de color antiguos.

Los rangos de porcentaje también difieren y merecen su tabla, porque no hay forma de deducirlos:

Función Canal 100% equivale a
lab(), lch() L 100
lab() a, b 125
lch() C 150
oklab(), oklch() L 1
oklab() a, b 0.4
oklch() C 0.4

El defecto azul de CIELAB

CIELAB tiene cuarenta y ocho años y sigue siendo el idioma de la colorimetría industrial, pero su predicción del tono es floja en la región azul. Las líneas de tono constante se curvan, así que mantener el ángulo de lch() fijo no mantiene el tono percibido. La demostración más clara es un fundido de azul a blanco.

Interpola en línea recta desde #0000ff hasta #ffffff en coordenadas Lab y mide el tono de cada paso intermedio en OKLCH, que es el modelo que mejor lo predice:

Punto del fundido Vía Lab Tono OKLCH Vía Oklab Tono OKLCH
25% #7a4fff 287.2 #306dff 263.8
50% #af89ff 296.7 #74a3ff 262.8
75% #dac3ff 301.7 #b8d2ff 261.2

El azul de partida está en 264. Por Lab, el tono se desplaza casi cuarenta grados hacia el magenta y los colores intermedios son literalmente morados: #af89ff es un lavanda, no un azul claro. Por Oklab el tono se mueve tres grados en todo el recorrido.

Ese desvío es el problema que Oklab vino a resolver, y explica por qué la especificación eligió oklab —y no lab— como espacio por defecto para interpolar colores en transiciones y animaciones. Si hubiera elegido lab, cualquier transición que pase cerca del azul se iría al morado sin que nadie lo pidiera.

ℹ️
Cómo se construye Oklab, en tres pasos

De XYZ se pasa a un espacio LMS que aproxima la respuesta de los tres tipos de cono de la retina. A cada componente se le aplica una raíz cúbica. Los tres resultados se combinan con una matriz de 3x3 para obtener claridad y los dos ejes de oponencia. Eso es todo: dos matrices y tres raíces cúbicas, sin funciones a trozos. CIELAB, en cambio, tiene un tramo lineal cerca del negro para evitar la pendiente infinita del cubo en el origen. Esa simplicidad es intencionada: hace la conversión barata y la hace invertible sin casos especiales, cosa que importa cuando el navegador la ejecuta miles de veces por fotograma en un degradado.

Cuál usar

oklch() para todo lo que escribas a mano. Sus tres números corresponden a tres decisiones que sabes tomar y su comportamiento no te sorprende. Es el que quieres en tokens, en paletas y en cualquier color literal de tu hoja de estilos.

oklab() como espacio de interpolación. Cuando mezclas o haces un degradado y no quieres pensar en por qué lado gira el tono, las coordenadas cartesianas eliminan la pregunta. También es el que quieres si el recorrido pasa cerca del eje gris, donde el ángulo de tono se vuelve inestable.

lch() y lab() cuando hay un requisito externo. Un valor que viene de un perfil ICC, de un espectrofotómetro, de una guía de marca definida en Lab o de un flujo de imprenta llega en CIELAB. Escríbelo tal cual en lab() en lugar de convertirlo tú: el navegador lo hace mejor y sin pérdidas de redondeo. Fuera de eso no hay razón para usarlos.

Las dos familias son igual de compatibles: lab(), lch(), oklab() y oklch() están en los cuatro motores desde hace años. La elección es de criterio, no de soporte.

Por qué el tono se vuelve basura cerca del gris

Hay una patología que aparece en cuanto interpolas en un espacio polar y que explica bastantes degradados raros. C y H son coordenadas polares de un punto en el plano a-b. Cuando ese punto se acerca al origen, es decir cuando el color se acerca al gris, el ángulo se vuelve numéricamente inestable: una variación minúscula en a o en b produce un giro enorme en H. En el origen exacto el ángulo directamente no está definido, y lo que salga depende de cómo tu herramienta implemente la arcotangente de cero entre cero. Por eso el mismo gris aparece a veces como tono 0 y a veces como tono 89.88 según qué conversor uses: los dos son correctos porque el ángulo es irrelevante. El problema llega cuando interpolas atravesando esa zona. Un degradado de rojo a verde en oklch no pasa por el gris: el motor gira el ángulo de 29 a 142 manteniendo el croma alto, con lo que atraviesa el amarillo. Es una decisión legítima y a veces es justo lo que quieres, pero no es “el camino entre esos dos colores”: es el camino largo por el borde. En oklab la misma interpolación es una recta que sí pasa cerca del origen, con lo que el punto medio es un color apagado. Ninguno de los dos está mal; son dos preguntas distintas. La regla práctica que se deriva: si alguno de los dos extremos tiene croma bajo, interpola en un espacio cartesiano, porque el ángulo de ese extremo es ruido y arrastrarlo por toda la mezcla produce tintes que nadie pidió. Y si de verdad quieres la mezcla polar con un extremo gris, declara ese tono como none para que el motor tome el del otro color en vez de inventarse uno.

⚔️ Compara las dos familias
  1. Escribe el mismo color de marca en lch() y en oklch() y anota la diferencia de claridad.
  2. Construye una rampa de cinco pasos de azul a blanco en lch() con tono fijo y mírala. Repite en oklch().
  3. Comprueba que lab(53.59% 0 0) y oklch(59.99% 0 0) son el mismo gris.
  4. Coge un color medido de una guía de marca en Lab y escríbelo directamente en lab(). Compara con la conversión que hubieras hecho a mano.