El espacio de color OKLCH explicado — Por qué Tailwind v4 lo adoptó
Abra el código fuente de cualquier sistema de diseño de la era 2025 — shadcn/ui, Radix Themes, la paleta base de Tailwind v4 — y lo primero que salta a la vista son los colores. No códigos hex, no tripletas hsl(), sino una función de la que nadie hablaba hace tres años: oklch(). Tailwind v4 publica toda su paleta por defecto como literales OKLCH. shadcn ahora genera temas que emiten propiedades personalizadas OKLCH. El sistema de diseño de Vercel se reconstruyó alrededor de ello en 2024.
Esto no es seguir la moda. Hay una razón específica y matemática por la que todo sistema de diseño serio ha cambiado discretamente de modelo de color, y una vez que la ve, no podrá dejar de ver por qué HSL siempre fue la herramienta equivocada para lo que la usábamos.
Esta publicación recorre esa razón desde los primeros principios, termina con una conversión paso a paso de un código hex a OKLCH, y le entrega una receta de migración para su propia paleta.
Cuando los espacios de color se rompieron
Los sistemas de diseño tienen un trabajo tonal. Un botón pasa al pasar el cursor a un estado ligeramente más claro que su estado de reposo. Una tarjeta atenuada se sitúa un escalón más oscura que la superficie que la rodea. Un anillo de foco necesita ser visiblemente más brillante que el cromado neutro que tiene detrás. Hacer esto bien, a escala, requiere que “más claro” y “más oscuro” signifiquen lo mismo en cada matiz de su paleta.
Ese requisito era fácil de ignorar cuando las paletas tenían ocho colores y tres estados. Se volvió incómodo cuando los equipos empezaron a entregar rampas de 11 pasos (50–950 en la convención de Tailwind), ocho colores semánticos, variantes claras y oscuras, y acentos de marca que tenían que coexistir con los colores del sistema de iOS, Android y la Web. De repente la pregunta de “¿este teal-500 tiene la misma luminosidad que nuestro blue-500?” era un problema real de ingeniería, no un lujo de dirección artística.
HSL — el caballo de batalla desde CSS 3 — no podía responderla. Dos colores HSL con valores L idénticos pueden verse drásticamente diferentes en luminosidad percibida. Un amarillo HSL puro con lightness: 50% se ve mucho más brillante que un azul HSL puro con la misma luminosidad. Sus ojos no perciben el amarillo y el azul por igual; HSL fue diseñado para ser intuitivo para selectores, no perceptualmente consistente para rampas. Para 2023, cada sistema de diseño que escalaba más allá de un puñado de colores estaba parchando esto con scripts de mezcla personalizados o sobreescrituras ajustadas a mano.
Lo que necesitábamos era un modelo de color donde L realmente significara “la luminosidad percibida que un humano reportaría,” y donde rotar el matiz o reducir la saturación no cambiara invisiblemente la luminosidad como efecto colateral. Ese modelo existía en la ciencia del color académica — simplemente aún no había llegado a CSS.
El problema de HSL, en concreto
Suelte estos en un navegador y mírelos lado a lado:
.a { background: hsl(60 100% 50%); } /* yellow */
.b { background: hsl(240 100% 50%); } /* blue */
.c { background: hsl(120 100% 50%); } /* green */
Los tres tienen L: 50%. Ninguno parece tener la misma luminosidad. El amarillo casi quema; el azul se lee casi negro contra una página blanca; el verde se sitúa entre ellos. Si construye un estado hover sumando 10% a L, el hover del amarillo es apenas visible mientras que el del azul es un cambio drástico. Su pulido de interacción acaba dependiendo del matiz del que el diseñador haya partido.
Esto no es un error de HSL. HSL fue diseñado en 1978 para selectores de color al estilo “pinta por números”, donde los usuarios manipulaban matiz, saturación y “luminosidad” — definida como (max(R,G,B) + min(R,G,B)) / 2 — para afinar un color. Las matemáticas no tienen noción alguna de la percepción humana. La luminosidad en HSL es un punto medio geométrico de los canales sRGB, nada más.
La CIE — el organismo internacional de normalización de la colorimetría — conocía este problema desde la década de 1970. Publicaron dos espacios perceptualmente uniformes, CIELAB y CIELUV, que definían la luminosidad como algo más cercano a lo que realmente hace la visión humana. Para los años 1990, CIE LAB era el estándar en impresión, fotografía y gestión de color. Pero su conversión a RGB es complicada, y CSS nunca lo adoptó ampliamente. Los desarrolladores web continuaron usando HSL no porque fuera correcto sino porque estaba ahí.
CIE LAB / LCH: la corrección académica, con sus propios problemas
CIELAB toma un valor tristímulo XYZ (un modelo de cómo responden los conos humanos a la luz) y lo pasa por una raíz cúbica y una rotación 2D para producir tres canales: L* (luminosidad, 0–100), a* (verde ↔ rojo), y b* (azul ↔ amarillo). LCH es el mismo espacio expresado en forma polar: L*, C* (chroma, distancia desde el neutro), H* (ángulo de matiz).
Estos espacios son perceptualmente uniformes en un sentido medible. Un ΔE de 1 — un paso unitario en cualquier dirección en el espacio LAB — es aproximadamente la diferencia de color más pequeña que un observador entrenado puede detectar. Los flujos de impresión y preimpresión han funcionado sobre LAB y LCH durante décadas.
Entonces, ¿por qué CSS no adoptó simplemente LCH y siguió adelante?
Dos razones. Primero, CIE LAB se calibró contra una condición de observación específica (un observador estándar de 2° bajo iluminación D50) optimizada para reflectancia de superficies, no para pantallas emisivas. En pantallas, su uniformidad perceptual se desvía — los colores que son “igualmente brillantes” en LAB no siempre se ven igualmente brillantes en un teléfono. Segundo, la gama LCH es incómoda. Hay colores visibles que LAB describe bien pero que quedan fuera de las gamas comunes de pantalla, y la asignación de LCH a sRGB produce ocasionalmente desplazamientos de matiz (su azul se vuelve ligeramente púrpura cuando reduce su chroma). Para el trabajo de sistemas de diseño, ambos son obstáculos insalvables.
CSS Color 4 sí añadió lab() y lch() en 2021, y funcionan en navegadores modernos. Pero para el problema específico de construir rampas tonales consistentes en pantallas emisivas, la comunidad siguió buscando.
OKLAB / OKLCH: la idea de Ottosson en 2020
En diciembre de 2020, Björn Ottosson — un ingeniero de color sueco — publicó un artículo titulado “A perceptual color space for image processing”. El artículo era breve: tres matrices cortas, un paso de raíz cúbica, sin tablas de calibración, sin datos de referencia con derechos de autor. Ottosson tomó los modelos de color IPT y CAM16-UCS existentes — espacios académicos con buenas propiedades pero matemáticas difíciles — y derivó un espacio más simple que aproximaba su comportamiento perceptual usando multiplicaciones ordinarias de matrices sobre valores tristímulo XYZ de luz lineal.
Lo llamó OKLAB. La forma polar es OKLCH.
Lo que hace especial a OKLCH no es la novedad — es ser adecuado al propósito. Tres propiedades juntas:
- La luminosidad en OKLCH es genuinamente perceptual. Un amarillo puro con
L: 0.7y un azul puro conL: 0.7se ven con la misma luminosidad en una pantalla calibrada. Los estados hover definidos comoL + 0.05producen cambios visualmente equivalentes en toda la paleta. - El matiz se preserva bajo cambios de chroma. Si reduce el valor C de
oklch(0.7 0.2 30)aoklch(0.7 0.1 30), el matiz se queda quieto. En LCH, la misma operación a menudo introduce un desplazamiento de matiz visible. En OKLCH, puede aplanar el chroma para construir una variante atenuada de un color de marca sin que se desvíe accidentalmente hacia un matiz diferente. - Las matemáticas son baratas. Dos multiplicaciones de matrices y una raíz cúbica. Implementable en una función JavaScript de 30 líneas. Sin tablas de búsqueda, sin calibración por dispositivo, sin preocupaciones de licencias.
La combinación es lo que hizo que OKLCH fuera utilizable en CSS real. El W3C añadió oklch() a CSS Color 4 en 2022. Chrome 111 lo incluyó en 2023. Todo navegador evergreen lo soportaba para mediados de 2024. Tailwind v4 lo convirtió en el formato de paleta por defecto el mismo año.
Las matemáticas: una conversión HEX → OKLCH paso a paso
Recorramos #3b82f6 — el blue-500 de Tailwind — hasta OKLCH. Estas son las mismas matemáticas que ejecutan el Convertidor de Color y el spoke HEX a OKLCH con cada pulsación de tecla. Saber qué pasa bajo el capó hará que los detalles delicados de la siguiente sección tengan más sentido.
Paso 1: Hex a sRGB. Divida el hex de seis dígitos en tres pares y divida cada uno por 255.
const r = 0x3b / 255; // 0.231
const g = 0x82 / 255; // 0.510
const b = 0xf6 / 255; // 0.965
Estos son valores sRGB codificados con gamma: los valores de canal tal como los almacena su archivo de imagen, con una curva no lineal incorporada para compensar cómo emiten luz los monitores.
Paso 2: sRGB a sRGB lineal. Elimine la curva de gamma para tener valores de canal de luz lineal. La transformación estándar por tramos de CSS Color 4 §11.2:
const linear = (v) => v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
const [lr, lg, lb] = [r, g, b].map(linear);
// lr ≈ 0.045, lg ≈ 0.220, lb ≈ 0.923
Paso 3: sRGB lineal a XYZ D65. Una multiplicación de matrices estándar, definida en CSS Color 4 §15.1:
const x = 0.4124564 * lr + 0.3575761 * lg + 0.1804375 * lb;
const y = 0.2126729 * lr + 0.7151522 * lg + 0.0721750 * lb;
const z = 0.0193339 * lr + 0.1191920 * lg + 0.9503041 * lb;
// x ≈ 0.265, y ≈ 0.231, z ≈ 0.927
Esta es la representación tristímulo XYZ canónica — la forma “qué longitudes de onda es este color, en términos de respuesta de los conos humanos”.
Paso 4: XYZ a LMS. La primera matriz de Ottosson asigna XYZ a un espacio de fundamentos de conos largos/medios/cortos sintonizado para OKLAB:
const lms = [
0.8189330101 * x + 0.3618667424 * y - 0.1288597137 * z,
0.0329845436 * x + 0.9293118715 * y + 0.0361456387 * z,
0.0482003018 * x + 0.2643662691 * y + 0.6338517070 * z,
];
Paso 5: Raíz cúbica de los valores LMS. Este es el paso de compresión perceptual — análogo a la raíz cúbica en CIE LAB:
const lms_ = lms.map(Math.cbrt);
Paso 6: LMS’ a OKLAB. La segunda matriz de Ottosson:
const L = 0.2104542553 * lms_[0] + 0.7936177850 * lms_[1] - 0.0040720468 * lms_[2];
const a = 1.9779984951 * lms_[0] - 2.4285922050 * lms_[1] + 0.4505937099 * lms_[2];
const b_ = 0.0259040371 * lms_[0] + 0.7827717662 * lms_[1] - 0.8086757660 * lms_[2];
// L ≈ 0.629, a ≈ -0.022, b_ ≈ -0.191
Eso es OKLAB. El canal de luminosidad L ≈ 0.629 es lo que el ojo percibe como “60% de luminosidad” para este azul en particular.
Paso 7: OKLAB a OKLCH (cartesiano a polar).
const C = Math.sqrt(a * a + b_ * b_); // 0.193
const H = (Math.atan2(b_, a) * 180 / Math.PI + 360) % 360; // 263.4
// → oklch(0.629 0.193 263.4)
Así que #3b82f6 es oklch(0.629 0.193 263.4). Luminosidad 0.629, chroma 0.193, matiz 263.4°.
Si construye una rampa 50–950 desde este color variando solo L (de 0.95 hasta 0.15 en 11 pasos), manteniendo C y H fijos, obtiene una paleta donde cada matiz visualmente es el mismo, desvaneciéndose uniformemente en luminosidad. Haga lo mismo en HSL y los tonos oscuros se desplazan hacia el púrpura mientras que los claros se vuelven grises. Esa es la victoria.
Display P3 + Rec2020: por qué OKLCH desbloquea la gama amplia
OKLCH no tiene límites. A diferencia de HSL (limitado a sRGB) e incluso LCH (calibrado para superficies reflectantes), OKLCH no tiene gama implícita. Puede escribir oklch(0.7 0.25 30) y producir un rojo vivo que cabe dentro del volumen de color de Display P3 pero fuera del de sRGB. En un iPhone o MacBook reciente, se renderiza. En un monitor más antiguo, el navegador lo ajusta automáticamente a la representación sRGB más cercana.
Esto importa porque Apple, Samsung y el W3C pasaron la segunda mitad de la década de 2010 lanzando hardware de gama amplia. El MacBook Pro 14” / 16” con mini-LED viene con P3 por defecto. El iPhone 15 Pro renderiza Display P3 en Safari. Los teléfonos Android de gama alta vienen con paneles Rec2020. Para 2025, una fracción sustancial del tráfico de sistemas de diseño está en hardware de gama amplia que puede mostrar colores que HSL/sRGB simplemente no pueden expresar.
OKLCH le permite escribir esos colores sin atornillar una declaración @media (color-gamut: p3) separada. El navegador se encarga del fallback. Su sistema de diseño obtiene un “usa el rojo más brillante que el dispositivo pueda renderizar” listo para usar.
Esta es también la razón por la que OKLCH es el formato correcto para los tokens de diseño. Una variable --brand en OKLCH es una descripción independiente del dispositivo de la intención. El navegador descubre qué renderizar en cualquier pantalla que el usuario tenga, y su código es portable a través de CSS, SwiftUI (que soporta nativamente Display P3 Color), Android Compose (consciente de Rec2020) y Flutter.
Tailwind v4 y la revolución de los tokens de diseño
Tailwind v4 — lanzado en 2024 — fue el punto de inflexión que convirtió OKLCH de investigación en estándar de la industria. Los autores de Tailwind tomaron tres decisiones opinadas:
- La paleta por defecto es OKLCH. Slate, gray, zinc, neutral, stone — cada color de Tailwind se define en
oklch()en el código fuente. Las rampas 50–950 son uniformes en luminosidad perceptual por construcción. - Los temas personalizados usan bloques
@themecon literales OKLCH. Los colores de marca se definen como tokensoklch(); las utilidades derivadas (bg-brand-500,text-brand-300) se generan. - No se requiere ceremonia de fallback. Los navegadores sin soporte para OKLCH están por debajo de la línea base documentada de Tailwind v4.
Esa última decisión fue la que convirtió la adopción en una elección sin trampas. Tan recientemente como en 2023, los diseñadores tenían que entregar versiones tanto oklch() como hsl() de cada color para que las versiones más antiguas de Safari no se rompieran. Con Tailwind v4, la línea base son navegadores de 2023 o más nuevos, y OKLCH funciona en todas partes.
El generador de temas de shadcn/ui sigue el mismo patrón: introduzca su color de marca, obtenga una rampa OKLCH. El sistema de diseño de Vercel usa OKLCH para sus colores semánticos. Las escalas de color de Radix Themes se definen en OKLCH. La comunidad ha convergido.
Migración práctica: paleta HEX → paleta OKLCH
Si hoy tiene una paleta basada en hex, la migración es mecánica. Aquí está la receta:
1. Decida la estructura de su rampa. La 50–950 de Tailwind (11 paradas) es el estándar de facto y vale la pena seguirla a menos que tenga una razón específica para no hacerlo. Paradas en L = 0.97, 0.93, 0.86, 0.76, 0.63, 0.50, 0.42, 0.34, 0.26, 0.18, 0.10 dan una rampa perceptual suave.
2. Convierta su hex de marca a OKLCH. Use el Convertidor de Color o la herramienta HEX a OKLCH. Obtendrá una tripleta como oklch(0.629 0.193 263.4). Anote el valor H — ese es su matiz de marca.
3. Mantenga C y H constantes; varíe L. Construya la rampa emitiendo:
--brand-50: oklch(0.97 0.193 263.4);
--brand-100: oklch(0.93 0.193 263.4);
...
--brand-950: oklch(0.10 0.193 263.4);
4. Ajuste las paradas extremas. Con L muy bajo (≤ 0.20) y L muy alto (≥ 0.95), los valores altos de chroma quedan fuera de sRGB. Reduzca C para esas paradas, o acepte el ajuste automático del navegador. Los valores por defecto de Tailwind reducen el chroma hacia ambos extremos — copie ese patrón.
5. Defina alias semánticos. --surface: var(--brand-50), --surface-elevated: var(--brand-100), --text-primary: var(--brand-900), etc. Ahora sus tokens de diseño se leen como intención, no como colores.
6. Verifique el contraste. Use APCA Lc o las razones de contraste WCAG 2 para confirmar que cada par --text-* contra cada par --surface-* cumple su umbral de accesibilidad. Como la L de OKLCH es perceptual, las matemáticas de contraste son más fiables de lo que serían en HSL.
Un equipo que ejecute esta migración sobre una paleta heredada de 60 colores normalmente aterriza una paleta OKLCH más pequeña y uniforme de 30–40 tokens en una sola tarde. La nueva paleta se entrega más pequeña, genera menos CSS y produce visiblemente mejor movimiento tonal en estados hover y deshabilitados sin ningún ajuste extra.
Trampas y cómo manejarlas
Algunas cosas que debe saber al entrar:
Advertencias de fuera de gama. Algunos valores OKLCH caen fuera de Display P3 o sRGB. Los navegadores modernos manejan automáticamente el ajuste al color válido más cercano, pero el ajuste tiene pérdida: su oklch(0.7 0.25 30) puede renderizarse ligeramente menos saturado de lo que escribió. Herramientas como la fila de gama del Convertidor de Color le dicen si su color es seguro en sRGB, P3 o Rec2020, y ofrecen un ajuste a sRGB de un clic para que lo que escribe sea lo que ve.
Rarezas de chroma sub-píxel. El chroma OKLCH es ilimitado, pero el rango útil es aproximadamente 0 a ~0.4 para colores visibles. Valores por encima de 0.4 solo son alcanzables con luz láser monocromática — no son colores físicos que una pantalla pueda renderizar. El Convertidor de Color limita el control deslizante de chroma a 0.4 por esta razón; valores más allá no producen ninguna diferencia perceptible en ninguna pantalla real.
Soporte de navegadores, estilo 2025. Chrome 111+, Safari 15.4+, Firefox 113+ soportan oklch() nativamente. Los navegadores anteriores a 2023 no. Si tiene que dar soporte a IE/Edge heredados o a Safari móvil antiguo (1–3% del tráfico según su audiencia), puede emparejar una declaración OKLCH con un fallback hex usando @supports (color: oklch(0 0 0)) — pero para los tokens de sistemas de diseño que se entregan en 2025, el coste del fallback a menudo supera al beneficio heredado.
Permanencia del código hex. OKLCH es para la intención del sistema de diseño. Su CMS puede seguir necesitando un valor hex por razones heredadas (firmas de correo, documentos de Office, listas de verificación de activos de marca). Mantenga una tabla de búsqueda generada que emita el hex ajustado a sRGB para cada token OKLCH, pero no escriba directamente en hex.
No confunda OKLCH y OKLAB. OKLAB es la forma rectangular (canales L, a, b); OKLCH es el mismo espacio de color en forma polar (L, C, H). Se convierten entre sí con un paso cartesiano↔polar. Use OKLCH para tokens (más legible, más fácil de rampear); use OKLAB internamente si necesita interpolar o mezclar colores.
Pruébelo en su propia paleta
La forma más rápida de ver en acción lo que hemos descrito aquí es soltar un hex de marca en el Convertidor de Color. Escriba el color de su marca en el campo HEX y lea la salida OKLCH. Después mueva los controles deslizantes en el lado OKLCH y observe cómo el mismo matiz se queda igual cuando reduce el chroma, y cómo la misma luminosidad se queda igual cuando rota el matiz. Después de unos minutos tendrá un sentido intuitivo de por qué HSL siempre fue la herramienta equivocada para rampas tonales, y por qué cada sistema de diseño serio ha avanzado.
Para la conversión específica HEX a OKLCH, use HEX a OKLCH — las mismas matemáticas que este artículo, con un beneficio adicional: le muestra la clasificación de gama (sRGB / Display P3 / Rec2020) para que sepa cuáles de sus colores de marca son seguros en todas partes frente a cuáles necesitan un dispositivo de gama amplia para renderizar completamente.
Eso es OKLCH. Vale la pena la migración. Bien hecha, nunca volverá a escribir hsl().