O que é um espaço de cor
Uma tela produz cor misturando três luzes: vermelho, verde e azul. Isso é síntese aditiva, apagados, dão preto; no máximo, dão branco. Um espaço de cor é apenas um sistema de coordenadas para apontar uma cor específica dentro do que o dispositivo consegue exibir. Quase toda a web usa o gamut sRGB, o conjunto de cores padrão de monitores comuns; HEX, RGB, HSL e HSV são notações diferentes para pontos dentro desse mesmo gamut.
Duas distinções organizam tudo o que vem a seguir. A primeira: trocar de notação (RGB para HSL) não muda a cor, só a forma de descrevê-la; já trocar de gamut (sRGB para Display P3) pode mudar quais cores existem. A segunda, menos óbvia e o coração deste guia: descrever uma cor não é o mesmo que medir como ela é percebida. HEX, RGB, HSL e HSV descrevem bem; nenhum deles mede brilho percebido. Para isso existe uma linhagem paralela de espaços, CIELAB, e mais recentemente o Oklab, construídos a partir de como o olho humano realmente enxerga.
- 1931CIE XYZ e o observador padrão
A CIE mapeia toda cor visível a partir de experimentos de correspondência com observadores humanos, criando o espaço XYZ, a base matemática de todos os outros.
- 1976CIELAB (CIE L*a*b*)
A CIE publica o L*a*b*, o primeiro espaço pensado para ser perceptualmente uniforme: distâncias iguais deveriam parecer diferenças iguais.
- 1996sRGB vira o padrão da web
HP e Microsoft propõem o sRGB como espaço padrão da internet; ele vira a norma IEC 61966-2-1 em 1999. É o gamut que HEX, RGB e HSL assumem por padrão.
- 2020Oklab
Björn Ottosson publica o Oklab, corrigindo a previsão de matiz e luminosidade do CIELAB, sobretudo nos azuis, para a gama de uma tela.
- 2022–2023CSS Color 4 nos navegadores
oklch() e oklab() chegam ao Safari 15.4 (2022) e a Chrome, Edge e Firefox em 2023; viram Baseline "amplamente disponível" em maio de 2023.
HEX e RGB: a mesma cor por canal
RGB dá a intensidade de cada canal de 0 a 255. rgb(51, 102, 204) significa: vermelho baixo, verde médio, azul alto, um azul-royal. HEX é a mesma coisa em base 16 (hexadecimal): cada par de dígitos é um canal de 00 a FF, ou seja, 0 a 255. Por isso #3366CC = rgb(51, 102, 204): 33 em base 16 é 51, 66 é 102, CC é 204. Não há informação nova no HEX, só uma escrita mais curta, ideal para colar no CSS.
HEX -> RGB (cada par e um canal, base 16 -> base 10)
33 = 3*16 + 3 = 51
66 = 6*16 + 6 = 102
CC = 12*16 + 12 = 204
#3366CC = rgb(51, 102, 204)
HEX de 3 digitos: #39C -> #3399CC (cada digito e duplicado)
HEX de 8 digitos: #3366CC80 -> os dois ultimos (80) sao o alfaDuas conveniências do HEX: a forma curta de 3 dígitos (#39C) duplica cada dígito para virar 6, e a forma de 8 dígitos acrescenta um par de alfa no fim. O alfa 80 em base 16 é 128, que dividido por 255 dá cerca de 0,50, meia transparência. A sintaxe moderna do CSS Color 4 também aceita canais separados por espaço, com o alfa depois de uma barra: rgb(51 102 204 / 50%) é a forma atual de escrever rgba(51, 102, 204, 0.5). Pense em RGB/HEX quando você fala a língua da máquina: canais, telas, código.
HSL: intuitivo, mas mente sobre brilho
Ninguém pensa "preciso de mais 40 de verde e menos 20 de azul". Pensamos "quero esse mesmo azul, só que mais claro". HSL (Hue, Saturation, Lightness) reorganiza a cor em torno dessa intuição: o matiz (H) é um ângulo de 0 a 360° numa roda; a saturação (S) vai de cinza a cor pura; e a luminosidade (L) supostamente vai de preto a branco. O detalhe fatal está no L. Ele não é medido a partir do olho: é uma reprojeção trivial do RGB, a média aritmética do canal mais alto e do mais baixo. Pura geometria do cubo RGB.
L = (max + min) / 2- max
- maior dos três canais normalizados (0 a 1)
- min
- menor dos três canais normalizados (0 a 1)
Y = 0.2126 * R_lin + 0.7152 * G_lin + 0.0722 * B_lin- Y
- luminância relativa, de 0 (preto) a 1 (branco)
- R_lin, G_lin, B_lin
- canais linearizados (com a curva de gama do sRGB desfeita)
- 0.2126 / 0.7152 / 0.0722
- pesos da sensibilidade humana ao vermelho, verde e azul
As duas fórmulas não têm nada em comum. O L do HSL não conhece esses pesos: para ele, vermelho, verde e azul "puros" são todos igualzinhos, um L de 50%. A luminância relativa sabe que o olho é dez vezes mais sensível ao verde do que ao azul. O resultado é a prova de que o HSL mente: um punhado de cores com o mesmo L de 50% emite quantidades de luz radicalmente diferentes. Abaixo, a luminância real de seis cores que o HSL insiste em chamar de "50% claras".
Ver os dados
| Categoria | Valor |
|---|---|
| Amarelo #FFFF00 | 92,78% |
| Ciano #00FFFF | 78,74% |
| Verde #00FF00 | 71,52% |
| Magenta #FF00FF | 28,48% |
| Vermelho #FF0000 | 21,26% |
| Azul #0000FF | 7,22% |
Conversão passo a passo (e a luminância que o HSL esconde)
Vamos converter #3366CC até HSL e, na sequência, calcular a luminância real que ele carrega, a conta que o HSL nunca mostra. Primeiro normalizamos cada canal dividindo por 255; daí saem luminosidade, saturação e matiz por geometria; depois linearizamos os canais e aplicamos os pesos perceptuais:
#3366CC -> rgb(51, 102, 204)
1) Normaliza (canal / 255)
R = 51/255 = 0.20 G = 102/255 = 0.40 B = 204/255 = 0.80
2) HSL (geometria do cubo RGB)
max = 0.80 min = 0.20 delta = 0.60
L = (max + min) / 2 = 0.50 -> 50%
S = delta / (1 - |2L - 1|) = 0.60 -> 60%
H = ((R - G)/delta + 4) * 60 = 220 graus
=> hsl(220, 60%, 50%)
3) Luminancia relativa (lineariza, depois pesa)
lin(c) = ((c + 0.055)/1.055)^2.4 para c > 0.04045
R_lin = 0.0331 G_lin = 0.1328 B_lin = 0.6038
Y = 0.2126*0.0331 + 0.7152*0.1328 + 0.0722*0.6038
Y = 0.146 -> 14.6% da luz do branco
O HSL diz L = 50%. A luminancia real e 14.6%. Nao sao a mesma coisa.Repare no salto do passo 3. O HSL declara "L = 50%" para o #3366CC, mas o pixel só emite 14,6% da luz de um branco. A culpa é dupla: o azul, canal dominante, tem o menor peso perceptual (0,0722), e a linearização derruba ainda mais os valores intermediários (um canal em 0,80 vira 0,60 de luz; um em 0,40 vira só 0,13). O HSL ignora as duas coisas. Agora o par que fecha o argumento, dois HEX com o mesmo L e luminâncias em lados opostos da escala:
- Amarelo #FFFF00rgb(255, 255, 0), normalizado (1, 1, 0). max = 1, min = 0, então L = (1 + 0) / 2 = 0,50, o HSL diz 50%. Ao linearizar, o canal em 1 continua 1 e o canal em 0 continua 0, então Y = 0,2126×1 + 0,7152×1 + 0,0722×0 = 0,9278, ou seja, 92,78% da luz do branco.
- Azul #0000FFrgb(0, 0, 255), normalizado (0, 0, 1). max = 1, min = 0, então L = 0,50, o HSL também diz 50%. Mas Y = 0,2126×0 + 0,7152×0 + 0,0722×1 = 0,0722, só 7,22% da luz do branco.
- O vereditoDois HSL idênticos no L (50%), e o amarelo emite quase 13 vezes mais luz que o azul. É por isso que o mesmo "L" produz brilhos percebidos completamente diferentes, e por que um texto azul pode reprovar num contraste que o mesmo tom em amarelo passaria com folga. O "50% do HSL" não é uma promessa sobre brilho; é só um ponto no meio do cubo.
| HEX | RGB | HSL | HSV | Luminância Y |
|---|---|---|---|---|
| #3366CC | rgb(51, 102, 204) | hsl(220, 60%, 50%) | hsv(220, 75%, 80%) | 14,6% |
| #FFFF00 | rgb(255, 255, 0) | hsl(60, 100%, 50%) | hsv(60, 100%, 100%) | 92,8% |
| #0000FF | rgb(0, 0, 255) | hsl(240, 100%, 50%) | hsv(240, 100%, 100%) | 7,2% |
OKLCH: quando o "L" volta a significar brilho
Se o problema é que o L do HSL não corresponde ao brilho, a solução é usar um espaço cujo eixo de luminosidade seja perceptual. Foi o que Björn Ottosson publicou em 2020 com o Oklab, e sua versão em coordenadas polares, o OKLCH (Lightness, Chroma, Hue). Ottosson otimizou o espaço numericamente sobre um grande conjunto de cores visualmente semelhantes, corrigindo justamente onde o CIELAB errava mais: a previsão de matiz nos azuis. Em OKLCH, um L de 0,5 realmente parece meio caminho entre o preto e o branco, para qualquer matiz. O CSS Color 4 adotou tudo isso: oklch(), oklab(), lab(), lch() e a função color() para gamuts amplos.
HSL
- O L é geometria pura (média de max e min do RGB), não brilho percebido.
- Interpolar um gradiente em HSL atravessa zonas cinzentas ou "sujas".
- Girar o matiz muda o brilho junto: o mesmo L parece mais claro no amarelo que no azul.
- Útil como atalho rápido para clarear/escurecer dentro de um mesmo matiz.
OKLCH
- O L é luminosidade perceptual: 0,5 parece meio-caminho para o olho, em qualquer matiz.
- Interpolar em Oklab dá transições suaves, sem cinza morto, é o padrão do CSS Color 4.
- L, C e h são quase ortogonais: mexe-se em um sem estragar os outros dois.
- Base para paletas acessíveis: passos iguais de L viram passos iguais de contraste.
A diferença aparece de forma dramática em gradientes. A própria motivação de Ottosson era que "misturar duas cores deveria produzir transições regulares". Por isso o CSS Color 4 escolheu o Oklab como espaço de interpolação padrão de gradientes e da função color-mix(): a rota entre duas cores fica perceptualmente reta, sem o cinza que aparece ao interpolar no sRGB nem as bandas irregulares do HSL. Se você monta rampas de cor para um gerador de cores ou para tokens de design, é a diferença entre uma escala que "respira" igual e uma que trava no meio.
Por que gradientes em HSL (e sRGB) ficam cinzentos
Um gradiente de azul puro (0, 0, 255) até amarelo puro (255, 255, 0) interpolado direto no sRGB passa pelo ponto médio (127, 127, 127), cinza. Você pediu azul para amarelo e ganhou uma faixa lamacenta no meio, porque a média numérica de duas cores opostas é um cinza dessaturado.
Em HSL a interpolação de matiz evita o cinza, mas atravessa faixas onde o brilho percebido despenca e sobe, criando bandas "sujas". O Oklab foi desenhado para que o caminho entre duas cores seja perceptualmente reto, por isso o CSS Color 4 o usa como espaço de interpolação padrão de gradientes e color-mix().
A curva de gama do sRGB (por que é preciso linearizar)
Os valores 0–255 de um canal sRGB não são proporcionais à luz emitida. A codificação passa por uma função de transferência: um pequeno trecho linear (c ≤ 0,04045, onde a luz é c / 12,92) e depois uma potência, ((c + 0,055) / 1,055) elevado a 2,4, que juntos aproximam uma gama de cerca de 2,2. Só depois de desfazer essa curva os pesos 0,2126 / 0,7152 / 0,0722 fazem sentido físico.
Consequência prática: um cinza médio de canal 128 (meio do caminho em número) emite só cerca de 21% da luz de um branco, não 50%. A gama é também o motivo de redimensionar imagens sem linearizar deixar bordas escuras demais, o mesmo erro que o L do HSL comete ao ignorar a curva.
O que muda com Display P3 e Rec. 2020
sRGB é o menor dos gamuts modernos. O Display P3, usado em telas Apple e em muitos OLED, cobre cerca de 25% mais cores, vermelhos e verdes mais saturados que o sRGB simplesmente não alcança. O Rec. 2020, alvo de HDR e 8K, é ainda mais amplo.
No CSS você acessa esses gamuts com a função color(): color(display-p3 1 0 0) é um vermelho fora do sRGB. HEX, RGB e HSL, por definição, ficam presos ao sRGB, trocar de notação nunca alcança essas cores; só trocar de gamut alcança.
Qual usar quando
- HEX
- Padrão do CSS e dos design tokens: curto, sem vírgulas, fácil de copiar. Use para fixar cores estáticas.
- RGB(A)
- Quando você pensa em canais ou precisa de alfa explícito. A forma moderna é rgb(51 102 204 / 50%), com espaços e barra.
- HSL(A)
- Bom para ajustes rápidos de matiz e para variar tons dentro de uma cor, mas lembre que o L não é brilho percebido.
- OKLCH
- A escolha atual para paletas, gradientes e escalas acessíveis: L perceptual, matiz previsível, interpolação suave.
- CMYK / LAB
- CMYK é para impressão (tinta, síntese subtrativa, gamut menor); LAB serve para medir diferença de cor entre dois tons.
Uma nota sobre medir "quão diferentes" duas cores são: em espaços perceptuais como o LAB, a distância entre dois pontos é chamada ΔE. Uma diferença de cerca de 1 ΔE fica no limiar em que a diferença começa a ser perceptível lado a lado; alguns estudos situam a diferença apenas perceptível (JND) perto de 2,3. É por isso que dois azuis "quase iguais" no HEX podem ser óbvios ao olho, e dois amarelos "bem diferentes" no número podem parecer o mesmo tom, a distância no HEX não é a distância que você vê.
O alfa é ortogonal a tudo isso: é a opacidade da cor, de 0 (transparente) a 1 (opaco), disponível como rgb(... / a), hsl(... / a) ou o par final do HEX de 8 dígitos. E cor na tela é sempre sRGB e aditiva, enquanto cor impressa é CMYK e subtrativa, com um gamut menor, por isso um laranja vibrante do monitor pode sair mais apagado no papel. Não é erro de conversão; é o gamut de impressão que não alcança aquela cor.
Perguntas frequentes
HEX e RGB são a mesma coisa?
Por que o "L" do HSL não é o brilho da cor?
O que é luminância relativa e como se calcula?
O que é OKLCH e por que usar em vez de HSL?
Por que gradientes às vezes ficam com uma faixa cinza no meio?
Por que minha cor fica diferente ao imprimir?
HEX, RGB, HSL e HSV são idiomas para a mesma cor sRGB, e todos descrevem bem, mas nenhum mede brilho. O "L" do HSL é a média de dois canais, não a luminância: amarelo e azul com o mesmo L de 50% diferem quase 13× na luz que emitem. Quem enxerga brilho de verdade é a luminância relativa (canais linearizados, pesados por 0,2126 / 0,7152 / 0,0722), a mesma base do contraste. Para trabalhar com brilho, gradiente e paleta do jeito que o olho vê, o caminho atual é o OKLCH do CSS Color 4.
Fontes e referências
- W3C, CSS Color Module Level 4
- Björn Ottosson, A perceptual color space for image processing (Oklab, 2020)
- W3C, A Standard Default Color Space for the Internet: sRGB (1996)
- W3C, WCAG 2.2: relative luminance (definição)
- MDN, oklch() (suporte e Baseline)
- MDN, color-mix() (interpolação padrão em Oklab)
- MDN, <color> (tipo de dado CSS)