OKLCH: por qué tu escala de grises se ve sucia en medio
Llevas un rato haciendo lo mismo: tomas el color de marca, generas la escala de 50 a 950 interpolando en hexadecimal, y los extremos salen bien. El 500 y el 600, en cambio, salen apagados, con un tinte gris que nadie pidió. Los subes un poco a mano, quedan bien, y seis meses después entra otra marca y repites el ajuste manual.
No es tu ojo. Es el espacio de color.
sRGB no fue diseñado para que interpolar signifique algo
#RRGGBB describe cuánto voltaje mandarle a tres fósforos. Es una descripción del hardware de un monitor de tubo de rayos catódicos de los noventa, y funciona perfectamente para eso. Lo que no hace es tener ninguna relación con cómo percibimos el color.
El promedio de dos hex no es el color que un humano llamaría "el de en medio". Interpolar de azul a rojo en sRGB pasa por un morado turbio; de amarillo a azul, por un verde grisáceo. La palabra técnica es que sRGB no es perceptualmente uniforme: la misma distancia numérica significa saltos visuales de tamaños distintos según dónde estés en el espacio.
HSL, que es lo que casi todos usamos para "aclarar un poco esto", no arregla nada — sólo reordena los mismos números en coordenadas polares. La prueba es un clásico:
Los dos declaran 50% de lightness. El amarillo es brillantísimo y el azul es casi negro. La L de HSL no mide luz percibida; mide la posición aritmética entre el negro y el color saturado. Por eso una escala generada bajando L en HSL se ve pareja en los números y desigual en pantalla.
OKLCH sí
OKLCH viene de , un espacio publicado por Björn Ottosson en 2020 que ajusta el modelo perceptual a datos experimentales modernos. OKLCH es Oklab en coordenadas polares, que son las que un humano puede razonar:
La diferencia que importa: en OKLCH, L: 0.7 se ve igual de luminoso en amarillo, en azul y en rojo. Es una promesa medible, no una impresión.
De ahí salen tres cosas gratis:
- Escalas parejas. Fijas
Len 0.97, 0.92, 0.85, … y los pasos se ven equidistantes. La escala de 11 tonos deja de necesitar retoques a mano. - Puntos medios limpios. Un degradado o una animación de color pasa por tonos vivos en vez de por el pozo gris. En CSS moderno lo pides explícito:
linear-gradient(in oklch, ...). - Cambiar el matiz sin recalcular nada. Mismo
LyC, muevesH, y toda la marca rota conservando su jerarquía de contraste. Esto es lo que hace viable un brand kit por cliente.
El bonus que nadie anuncia: colores que un hex no puede escribir
#RRGGBB sólo puede expresar lo que cabe en la gama sRGB. Las pantallas de los últimos años son P3, con un espacio de color notablemente más ancho — verdes y rojos que en hex simplemente no existen. OKLCH no está limitado a sRGB: puedes declarar un oklch(0.7 0.28 145) que en una pantalla P3 se ve saturadísimo y en una vieja cae al sRGB más cercano.
La contrapartida honesta: es fácil escribir un color fuera de gama sin darte cuenta. Subes el chroma buscando intensidad, el navegador lo recorta al borde de la gama, y en tu monitor bueno se ve espectacular mientras en el del cliente se ve plano. Vale la pena verificarlo con @supports (color: color(display-p3 1 1 1)) o revisar en una máquina sRGB antes de dar por buena una paleta.
Quién ya se movió, y qué implica para ti
- Tailwind v4 reescribió toda su paleta por defecto en OKLCH y usa P3 donde el navegador lo soporta. Si actualizaste a v4, tus colores ya cambiaron ligeramente — más vivos, sobre todo en los tonos medios.
- shadcn/ui migró sus tokens de tema a OKLCH.
- Radix Colors trabaja hace tiempo en espacios perceptuales para garantizar sus escalas de contraste.
Lo interesante es que no cambia ningún campo de tu UI. Sigue siendo un valor de color en una variable CSS; sigue entrando por la misma prop. Es un cambio de representación, no de arquitectura. El soporte de navegador está en todos los evergreen desde 2023.
Cómo migrar sin romper nada
El error es convertir los hex existentes a OKLCH uno por uno. Eso te deja los mismos colores con sintaxis nueva y cero beneficio — el problema no era la notación, era la escala.
Lo que sí funciona:
- Elige el color de marca y conviértelo. Ése es tu ancla:
L,C,Hreales. - Genera la escala fijando
Len una rampa —por ejemplo 0.97 / 0.94 / 0.89 / 0.82 / 0.74 / 0.66 / 0.58 / 0.49 / 0.40 / 0.30 / 0.20— y conservando elH. - Baja el chroma en los extremos. Los tonos casi blancos y casi negros con chroma alto se ven irreales; una curva que sube el
Chacia el centro de la escala y lo baja en las puntas se ve natural. - Calcula el texto por contraste, no a mano. Con
Lperceptual, decidir si un botón lleva texto claro u oscuro deja de ser una lista de excepciones y pasa a ser un umbral. Nosotros derivamos así el brand kit del workspace en Ghosty: una fuente de verdad y seis superficies que salen de ella. - Marca los degradados como
in oklch. Es una línea y es donde más se nota.
CSS moderno además te deja derivar sin herramientas, con color-mix() y oklch(from …):
Lo que no arregla
OKLCH resuelve la uniformidad perceptual de la luminosidad. No resuelve el contraste WCAG: la métrica del estándar sigue siendo una fórmula sobre sRGB, así que dos colores con la misma diferencia de L no necesariamente pasan el mismo criterio. Sigue midiendo el ratio; lo que ganas es que el ajuste sea de una variable y no de una tabla.
Tampoco te salva de una paleta mal elegida. Un sistema perceptualmente uniforme hace que los pasos malos sean parejos, no que sean buenos.
La tesis, sin metáfora: el color de una interfaz se manipula con operaciones —aclarar, oscurecer, mezclar, interpolar— y esas operaciones sólo dan resultados predecibles si el espacio en el que se hacen se corresponde con la percepción humana. sRGB describe un monitor; OKLCH describe lo que ves. Por eso el ecosistema se movió, y por eso el cambio no cuesta nada: la representación es lo único que cambia.