Skip to content
Voltar ao blog
Tutoriais

Espaço de Cor OKLCH Explicado — Por Que o Tailwind v4 Adotou

Por que OKLCH virou o padrão de design system em 2024–2026. Como ele difere de HSL e LCH, com uma conversão HEX→OKLCH passo a passo.

14 min de leitura

Espaço de Cor OKLCH Explicado — Por Que o Tailwind v4 Adotou

Abra o código-fonte de qualquer design system da era 2025 — shadcn/ui, Radix Themes, a paleta base do Tailwind v4 — e a primeira coisa que salta aos olhos são as cores. Não são códigos hex, não são triplas hsl(), mas uma função sobre a qual ninguém falava três anos atrás: oklch(). O Tailwind v4 entrega sua paleta padrão inteira como literais OKLCH. O shadcn agora gera temas que emitem propriedades customizadas OKLCH. O design system da Vercel se reconstruiu em torno disso em 2024.

Isto não é seguir modinha. Existe uma razão específica e matemática pela qual todo design system sério vem trocando de modelo de cor em silêncio, e quando você enxerga, não consegue mais desver por que o HSL sempre foi errado para o que a gente usava ele.

Este post percorre essa razão a partir dos primeiros princípios, termina com uma conversão completa de um código hex para OKLCH e te entrega uma receita de migração para sua própria paleta.

Quando os espaços de cor quebraram

Design systems têm um trabalho tonal. Um botão em hover fica ligeiramente mais claro que seu estado de repouso. Um card discreto fica um degrau mais escuro que a superfície ao redor. Um anel de foco precisa ser visivelmente mais brilhante que o cromo neutro atrás dele. Fazer isso bem, em escala, exige que “mais claro” e “mais escuro” signifiquem a mesma coisa em todos os matizes da sua paleta.

Esse requisito era fácil de ignorar quando as paletas tinham oito cores e três estados. Ele ficou incômodo quando os times começaram a entregar rampas de 11 passos (50–950 na convenção do Tailwind), oito cores semânticas, variantes claras e escuras e acentos de marca que tinham que coexistir com cores de sistema do iOS, Android e da Web. De repente a pergunta “este teal-500 tem a mesma luminosidade que o nosso blue-500?” virou um problema real de engenharia, não um luxo de direção de arte.

O HSL — o modelo cavalo de batalha desde o CSS 3 — não conseguia responder. Duas cores HSL com valores idênticos de L podem parecer dramaticamente diferentes em brilho percebido. Um amarelo HSL puro em lightness: 50% parece bem mais brilhante do que um azul HSL puro na mesma luminosidade. Seus olhos não percebem amarelo e azul igualmente; o HSL foi projetado para ser intuitivo para seletores, não perceptualmente consistente para rampas. Em 2023, todo design system que passava de um punhado de cores estava remendando o problema com scripts de mistura customizados ou overrides ajustados na mão.

O que precisávamos era um modelo de cor onde L de fato significasse “o brilho percebido que um humano reportaria”, e onde girar matiz ou reduzir saturação não mudasse invisivelmente o brilho como efeito colateral. Esse modelo existia na ciência acadêmica da cor — só não tinha chegado ao CSS ainda.

O problema do HSL, concretamente

Jogue isto em um navegador e olhe lado a lado:

.a { background: hsl(60 100% 50%); }   /* yellow */
.b { background: hsl(240 100% 50%); }  /* blue */
.c { background: hsl(120 100% 50%); }  /* green */

Os três têm L: 50%. Nenhum parece ter a mesma luminosidade. O amarelo quase queima; o azul lê quase preto contra uma página branca; o verde fica entre os dois. Se você constrói um estado de hover somando 10% no L, o hover do amarelo mal aparece enquanto o hover do azul é uma virada dramática. Seu polimento de interação acaba dependendo de qual matiz o designer começou.

Isto não é um bug do HSL. O HSL foi projetado em 1978 para seletores de cor por números, em que o usuário manipulava matiz, saturação e “lightness” — definida como (max(R,G,B) + min(R,G,B)) / 2 — para chegar a uma cor. A matemática não tem noção alguma de percepção humana. Luminosidade no HSL é um ponto médio geométrico dos canais sRGB, nada além disso.

A CIE — o órgão internacional de padrões de colorimetria — sabia desse problema desde os anos 1970. Eles publicaram dois espaços perceptualmente uniformes, CIELAB e CIELUV, que definiam luminosidade como algo mais próximo do que a visão humana de fato faz. Nos anos 1990, o CIE LAB era padrão em impressão, fotografia e gerenciamento de cor. Mas sua conversão para RGB é espinhosa, e o CSS nunca adotou em larga escala. Os desenvolvedores web continuaram usando HSL não porque estava certo, mas porque estava ali.

CIE LAB / LCH: a solução acadêmica, com seus próprios problemas

O CIELAB pega um valor de tristímulo XYZ (um modelo de como os cones humanos respondem à luz) e roda por uma raiz cúbica e uma rotação 2D para produzir três canais: L* (luminosidade, 0–100), a* (verde ↔ vermelho) e b* (azul ↔ amarelo). O LCH é o mesmo espaço expresso em forma polar: L*, C* (chroma, distância do neutro), H* (ângulo de matiz).

Esses espaços são perceptualmente uniformes em um sentido mensurável. Um ΔE de 1 — um passo unitário em qualquer direção no espaço LAB — é aproximadamente a menor diferença de cor que um observador treinado consegue detectar. Fluxos de impressão e pré-impressão rodam em LAB e LCH há décadas.

Então por que o CSS não simplesmente adotou LCH e seguiu em frente?

Duas razões. Primeiro, o CIE LAB foi calibrado contra uma condição de visualização específica (um observador padrão de 2° sob iluminação D50) otimizada para reflectância de superfície, não para telas emissivas. Em telas, sua uniformidade perceptual deriva — cores que estão “igualmente brilhantes” no LAB nem sempre parecem igualmente brilhantes em um celular. Segundo, a gama cromática do LCH é desajeitada. Existem cores visíveis que o LAB descreve bem mas que ficam fora das gamas comuns de exibição, e o mapeamento de LCH para sRGB ocasionalmente produz deslocamentos de matiz (seu azul vira ligeiramente roxo quando você reduz seu chroma). Para trabalho de design system, os dois são impeditivos.

O CSS Color 4 acabou adicionando lab() e lch() em 2021, e eles funcionam em navegadores modernos. Mas para o problema específico de construir rampas tonais consistentes em telas emissivas, a comunidade continuou procurando.

OKLAB / OKLCH: o insight de Ottosson em 2020

Em dezembro de 2020, Björn Ottosson — um engenheiro de cor sueco — publicou um paper intitulado “A perceptual color space for image processing”. O paper era pequeno: três matrizes curtas, um passo de raiz cúbica, sem tabelas de calibração, sem dados de referência protegidos por copyright. Ottosson pegou os modelos de cor IPT e CAM16-UCS existentes — espaços acadêmicos com boas propriedades mas matemática ruim — e derivou um espaço mais simples que aproximava o comportamento perceptual deles usando multiplicações comuns de matriz sobre valores de tristímulo XYZ de luz linear.

Ele chamou de OKLAB. A forma polar é OKLCH.

O que torna o OKLCH especial não é novidade — é adequação ao propósito. Três propriedades juntas:

  1. A luminosidade no OKLCH é genuinamente perceptual. Um amarelo puro em L: 0.7 e um azul puro em L: 0.7 parecem o mesmo brilho em uma tela calibrada. Estados de hover definidos como L + 0.05 produzem deslocamentos visualmente equivalentes pela paleta inteira.
  2. O matiz é preservado sob mudanças de chroma. Se você reduz o valor de C de oklch(0.7 0.2 30) para oklch(0.7 0.1 30), o matiz fica no lugar. No LCH, a mesma operação muitas vezes introduz um deslocamento de matiz visível. No OKLCH, você pode achatar o chroma para construir uma variante discreta de uma cor de marca sem que ela acidentalmente derive para um matiz diferente.
  3. A matemática é barata. Duas multiplicações de matriz e uma raiz cúbica. Implementável em uma função JavaScript de 30 linhas. Sem tabelas de lookup, sem calibração por dispositivo, sem preocupações de licenciamento.

A combinação foi o que tornou o OKLCH usável em CSS real. O W3C adicionou oklch() ao CSS Color 4 em 2022. O Chrome 111 entregou em 2023. Todo navegador evergreen passou a ter suporte na metade de 2024. O Tailwind v4 fez dele o formato padrão de paleta no mesmo ano.

A matemática: uma conversão HEX → OKLCH passo a passo

Vamos percorrer #3b82f6 — o blue-500 do Tailwind — até OKLCH. Esta é a mesma matemática que o Conversor de Cores e o spoke HEX para OKLCH rodam a cada tecla. Saber o que está acontecendo por baixo do capô vai fazer as armadilhas da próxima seção fazerem mais sentido.

Passo 1: Hex para sRGB. Divida o hex de seis dígitos em três pares e divida cada um por 255.

const r = 0x3b / 255; // 0.231
const g = 0x82 / 255; // 0.510
const b = 0xf6 / 255; // 0.965

Estes são valores sRGB codificados em gama: os valores de canal como seu arquivo de imagem armazena, com uma curva não-linear embutida para compensar como monitores emitem luz.

Passo 2: sRGB para sRGB linear. Remova a curva de gama para termos valores de canal em luz linear. A transformação padrão por trechos do 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

Passo 3: sRGB linear para XYZ D65. Uma multiplicação padrão de matriz, definida no 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 é a representação canônica de tristímulo XYZ — a forma “que comprimentos de onda é esta cor, em termos da resposta dos cones humanos”.

Passo 4: XYZ para LMS. A primeira matriz de Ottosson mapeia XYZ para um espaço de fundamentais de cone longo/médio/curto ajustado 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,
];

Passo 5: Raiz cúbica dos valores LMS. Este é o passo de compressão perceptual — análogo à raiz cúbica no CIE LAB:

const lms_ = lms.map(Math.cbrt);

Passo 6: LMS’ para OKLAB. A 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

Isso é OKLAB. O canal de luminosidade L ≈ 0.629 é o que o olho percebe como “60% de brilho” para este azul em particular.

Passo 7: OKLAB para OKLCH (cartesiano para 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)

Então #3b82f6 é oklch(0.629 0.193 263.4). Luminosidade 0.629, chroma 0.193, matiz 263.4°.

Se você constrói uma rampa 50–950 a partir desta cor variando só o L (de 0.95 a 0.15 em 11 passos), mantendo C e H fixos, você ganha uma paleta em que cada tom é visivelmente o mesmo matiz, desbotando em luminosidade de forma uniforme. Faça o mesmo no HSL e os tons escuros se deslocam para o roxo enquanto os claros viram cinza. Isso é o ganho.

Display P3 + Rec2020: por que OKLCH destrava gama ampla

OKLCH é ilimitado. Diferente do HSL (limitado ao sRGB) e até do LCH (calibrado para superfícies reflexivas), o OKLCH não tem gama implícita. Você pode escrever oklch(0.7 0.25 30) e produzir um vermelho vivo que cai dentro do volume cromático do Display P3 mas fora do sRGB. Em um iPhone ou MacBook recente, renderiza. Em um monitor mais antigo, o navegador encaixa automaticamente na representação sRGB mais próxima.

Isto importa porque Apple, Samsung e o W3C passaram o fim dos anos 2010 entregando hardware de gama ampla. O MacBook Pro 14” / 16” com mini-LED entrega P3 por padrão. O iPhone 15 Pro renderiza Display P3 no Safari. Os carros-chefe Android entregam painéis Rec2020. Em 2025, uma fração substancial do tráfego de design system está em hardware de gama ampla capaz de mostrar cores que HSL/sRGB simplesmente não conseguem expressar.

OKLCH te deixa escrever essas cores sem aparafusar uma declaração @media (color-gamut: p3) separada. O navegador cuida do fallback. Seu design system ganha um “use o vermelho mais brilhante que o aparelho consegue renderizar” de fábrica.

É também por isso que OKLCH é o formato certo para design tokens. Uma variável --brand em OKLCH é uma descrição independente de dispositivo de intenção. O navegador descobre o que renderizar em qualquer tela que o usuário tenha, e seu código fica portável entre CSS, SwiftUI (que suporta Display P3 Color nativamente), Android Compose (consciente de Rec2020) e Flutter.

Tailwind v4 e a revolução dos design tokens

O Tailwind v4 — lançado em 2024 — foi o ponto de inflexão que tirou o OKLCH da pesquisa e o transformou em padrão de indústria. Os autores do Tailwind tomaram três decisões opinativas:

  1. A paleta padrão é OKLCH. Slate, gray, zinc, neutral, stone — toda cor do Tailwind é definida em oklch() no código-fonte. As rampas 50–950 são uniformes em luminosidade perceptual por construção.
  2. Temas customizados usam blocos @theme com literais OKLCH. Cores de marca são definidas como tokens oklch(); utilitários derivados (bg-brand-500, text-brand-300) são gerados.
  3. Sem cerimônia de fallback exigida. Navegadores sem suporte a OKLCH ficam abaixo da linha de base documentada do Tailwind v4.

Esta última decisão foi a que tornou a adoção uma escolha sem armadilhas. Em 2023, designers ainda tinham que entregar versões oklch() e hsl() de toda cor para que Safaris antigos não quebrassem. Com o Tailwind v4, a linha de base são navegadores de 2023 ou mais novos, e o OKLCH funciona em todo lugar.

O gerador de temas do shadcn/ui segue o mesmo padrão: entre com sua cor de marca, saia com uma rampa OKLCH. O design system da Vercel usa OKLCH para suas cores semânticas. As escalas de cor do Radix Themes são definidas em OKLCH. A comunidade convergiu.

Migração prática: paleta HEX → paleta OKLCH

Se você tem uma paleta baseada em hex hoje, a migração é mecânica. Aqui está a receita:

1. Decida a estrutura da sua rampa. A 50–950 do Tailwind (11 paradas) é o padrão de fato e vale seguir a menos que você tenha um motivo específico para não. Paradas em L = 0.97, 0.93, 0.86, 0.76, 0.63, 0.50, 0.42, 0.34, 0.26, 0.18, 0.10 dão uma rampa perceptual suave.

2. Converta seu hex de marca para OKLCH. Use o Conversor de Cores ou o HEX para OKLCH. Você vai ganhar uma tripla como oklch(0.629 0.193 263.4). Anote o valor de H — este é o matiz da sua marca.

3. Mantenha C e H constantes; varie L. Construa a rampa emitindo:

--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 as paradas extremas. Em L muito baixo (≤ 0.20) e L muito alto (≥ 0.95), valores altos de chroma caem fora do sRGB. Reduza o C nessas paradas, ou aceite o encaixe automático do navegador. Os padrões do Tailwind reduzem chroma em direção aos dois extremos — copie esse padrão.

5. Defina aliases semânticos. --surface: var(--brand-50), --surface-elevated: var(--brand-100), --text-primary: var(--brand-900), e por aí vai. Agora seus design tokens leem como intenção, não como cores.

6. Confira o contraste. Use APCA Lc ou as razões de contraste WCAG 2 para confirmar que cada par --text-* contra cada par --surface-* atinge sua barra de acessibilidade. Como o L do OKLCH é perceptual, a matemática de contraste é mais confiável do que seria no HSL.

Um time rodando esta migração em uma paleta legada de 60 cores tipicamente fecha o dia com uma paleta OKLCH menor e mais uniforme de 30–40 tokens em uma única tarde. A paleta nova entrega menor, gera CSS menor e produz movimento tonal visivelmente melhor em estados de hover e desabilitado sem ajuste extra.

Armadilhas e como lidar com elas

Algumas coisas para saber antes de embarcar:

Avisos de fora da gama. Alguns valores OKLCH caem fora do Display P3 ou sRGB. Navegadores modernos cuidam do encaixe na cor válida mais próxima automaticamente, mas o encaixe é com perda: seu oklch(0.7 0.25 30) pode renderizar ligeiramente menos saturado do que você escreveu. Ferramentas como a linha de gama do Conversor de Cores te dizem se sua cor é sRGB-segura, P3-segura ou Rec2020-segura, e oferecem um snap-to-sRGB de um clique para que o que você escreve seja o que você vê.

Estranhezas de chroma sub-pixel. O chroma do OKLCH é ilimitado, mas a faixa útil é mais ou menos 0 a ~0.4 para cores visíveis. Valores acima de 0.4 só são atingíveis com luz laser monocromática — não são cores físicas que uma tela consiga renderizar. O Conversor de Cores limita o slider de chroma em 0.4 por essa razão; valores além disso não produzem diferença perceptível em nenhuma tela real.

Suporte de navegador, estilo 2025. Chrome 111+, Safari 15.4+, Firefox 113+ todos suportam oklch() nativamente. Navegadores pré-2023 não. Se você tem que dar suporte a IE/Edge legado ou Safari mobile mais antigo (1–3% do tráfego dependendo do seu público), você pode parear uma declaração OKLCH com um fallback hex usando @supports (color: oklch(0 0 0)) — mas para tokens de design system entregando em 2025, o custo do fallback geralmente pesa mais do que o benefício para o legado.

Permanência do código hex. OKLCH é para intenção de design system. Seu CMS ainda pode precisar de um valor hex por razões legadas (assinaturas de e-mail, documentos Office, checklists de asset de marca). Mantenha uma tabela de lookup gerada que emite o hex encaixado em sRGB para cada token OKLCH, mas não escreva em hex.

Não confunda OKLCH e OKLAB. OKLAB é a forma retangular (canais L, a, b); OKLCH é o mesmo espaço de cor em forma polar (L, C, H). Você converte entre eles com um passo cartesiano↔polar. Use OKLCH para tokens (mais legível, mais fácil de rampar); use OKLAB internamente se você precisa interpolar ou misturar cores.

Experimente na sua própria paleta

O jeito mais rápido de ver em ação o que descrevemos aqui é jogar um hex de marca no Conversor de Cores. Digite sua cor de marca no campo HEX e leia a saída OKLCH. Depois mova os sliders no lado OKLCH e veja como o mesmo matiz continua o mesmo matiz quando você reduz o chroma, e como a mesma luminosidade continua a mesma luminosidade quando você gira o matiz. Depois de alguns minutos você vai ter um senso intuitivo de por que o HSL sempre foi a ferramenta errada para rampas tonais, e por que todo design system sério seguiu em frente.

Para a conversão específica HEX-para-OKLCH, use o HEX para OKLCH — a mesma matemática deste artigo, com um benefício extra: ele te mostra a classificação de gama (sRGB / Display P3 / Rec2020) para você saber quais das suas cores de marca são seguras em todo lugar versus quais precisam de um dispositivo de gama ampla para renderizar por completo.

É isso, OKLCH. Vale a migração. Feita bem, você nunca mais escreve hsl().

Artigos relacionados

Ver todos os artigos