Design

Espaços de cor: HEX, RGB, HSL e por que o HSL mente sobre brilho

#3366CC, rgb(51, 102, 204) e hsl(220, 60%, 50%) são a mesma cor, escrita em três idiomas. Até aqui, nenhum mistério. O problema começa quando você confia no "L" do HSL: aquele "50% de luminosidade" não é o brilho que o seu olho percebe. Amarelo e azul podem ter exatamente o mesmo L de 50% em HSL e, ainda assim, o amarelo emitir quase treze vezes mais luz. Este guia mostra o que HEX, RGB e HSL de fato representam, faz as contas que provam que o HSL mente sobre brilho, calcula a luminância real e explica por que o CSS Color 4 trouxe o OKLCH para consertar isso. Teste cada conversão no [conversor de cores](tool:conversor-cores) enquanto lê.

J-Kit16 min de leituraIntermediário
  • Cores
  • CSS
  • HSL
  • OKLCH
  • sRGB

Resumo rápido

  • HEX e RGB são a mesma cor sRGB por canal; HSL e HSV apenas reprojetam esses canais, nenhum deles mede brilho percebido.
  • O "L" do HSL é geometria (a média do canal mais alto e do mais baixo), não luminância: amarelo e azul com L = 50% diferem quase 13× em luz real.
  • Luminância relativa pesa os canais já linearizados por 0,2126 / 0,7152 / 0,0722, é isso que o contraste e o olho enxergam, não o L do HSL.
  • OKLCH e OKLab (CSS Color 4) são perceptualmente uniformes: gradientes suaves e passos de L que viram passos de contraste. Baseline desde maio de 2023.

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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 alfa
HEX é RGB em hexadecimal; os atalhos de 3 e 8 dígitos.

Duas 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)
Luminosidade do HSL: só a média do canal mais alto e do mais baixo. Não pesa a sensibilidade do olho a cada cor.
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
Luminância relativa (WCAG / sRGB): pesa cada canal pela sensibilidade real do olho, depois de desfazer a gama. Repare: o verde vale ~10× mais que o 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".

Amarelo #FFFF0092,78%
Ciano #00FFFF78,74%
Verde #00FF0071,52%
Magenta #FF00FF28,48%
Vermelho #FF000021,26%
Azul #0000FF7,22%
Todas estas seis cores puras têm L = 50% em HSL. A luminância relativa real (branco = 100%) vai de 7% no azul a 93% no amarelo, um fator de quase 13×. É a prova visual de que o L do HSL não é brilho.
Ver os dados
CategoriaValor
Amarelo #FFFF0092,78%
Ciano #00FFFF78,74%
Verde #00FF0071,52%
Magenta #FF00FF28,48%
Vermelho #FF000021,26%
Azul #0000FF7,22%
92,78%luminância do amarelo, com HSL L = 50%
7,22%luminância do azul, com o mesmo HSL L = 50%
≈ 12,9×quanto o amarelo é mais claro que o azul, com o mesmo L

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.
De HEX a HSL por geometria, e depois a luminância real por perceção. O mesmo pixel: L = 50%, luminância = 14,6%.

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:

  1. 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.
  2. 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.
  3. 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.
A mesma cor em cada notação, e a luminância real que só a última coluna revela.
HEXRGBHSLHSVLuminância Y
#3366CCrgb(51, 102, 204)hsl(220, 60%, 50%)hsv(220, 75%, 80%)14,6%
#FFFF00rgb(255, 255, 0)hsl(60, 100%, 50%)hsv(60, 100%, 100%)92,8%
#0000FFrgb(0, 0, 255)hsl(240, 100%, 50%)hsv(240, 100%, 100%)7,2%
Reproduza as contas: digite #3366CC, #FFFF00 ou #0000FF e compare HSL, HSV e o valor Y do XYZ (a luminância relativa × 100).Abrir a ferramenta em página inteira

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?
Sim, descrevem a mesma cor sRGB. HEX é apenas RGB escrito em base 16: cada par de dígitos (00 a FF) é um canal (0 a 255). #3366CC e rgb(51, 102, 204) são idênticos, e a forma moderna rgb(51 102 204) também.
Por que o "L" do HSL não é o brilho da cor?
Porque o L é só a média do canal mais alto e do mais baixo do RGB, geometria, não perceção. Ele trata vermelho, verde e azul puros como igualmente claros (todos L = 50%), mas o olho é cerca de 10× mais sensível ao verde do que ao azul. Por isso amarelo e azul com L = 50% diferem quase 13× em luz real.
O que é luminância relativa e como se calcula?
É o brilho percebido de uma cor, de 0 (preto) a 1 (branco). Primeiro se lineariza cada canal (desfaz a gama do sRGB), depois se aplica Y = 0,2126×R + 0,7152×G + 0,0722×B. Os pesos vêm da sensibilidade do olho. É a mesma medida que o contraste usa, e o valor Y do XYZ no conversor é ela multiplicada por 100.
O que é OKLCH e por que usar em vez de HSL?
OKLCH é a forma polar (luminosidade, croma, matiz) do Oklab, um espaço perceptualmente uniforme criado por Björn Ottosson em 2020 e adotado pelo CSS Color 4. Diferente do HSL, o L do OKLCH corresponde ao brilho percebido em qualquer matiz, e interpolar cores nele gera gradientes suaves. Já funciona em todos os navegadores modernos (Baseline desde maio de 2023).
Por que gradientes às vezes ficam com uma faixa cinza no meio?
Porque interpolar duas cores opostas direto no sRGB passa pela média numérica, que é um cinza dessaturado, azul para amarelo cruza (127, 127, 127). Interpolar em Oklab evita isso, mantendo a rota perceptualmente reta. É por isso que o CSS Color 4 usa Oklab como espaço de interpolação padrão de gradientes e color-mix().
Por que minha cor fica diferente ao imprimir?
A tela usa sRGB (aditivo, luz); a impressão usa CMYK (subtrativo, tinta), com um gamut menor. Algumas cores vivas de tela não existem em CMYK, então saem mais apagadas, é limitação do gamut, não da conversão. Telas Display P3 e Rec. 2020 vão no sentido oposto: têm gamut maior que o sRGB.

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

  1. W3C, CSS Color Module Level 4
  2. Björn Ottosson, A perceptual color space for image processing (Oklab, 2020)
  3. W3C, A Standard Default Color Space for the Internet: sRGB (1996)
  4. W3C, WCAG 2.2: relative luminance (definição)
  5. MDN, oklch() (suporte e Baseline)
  6. MDN, color-mix() (interpolação padrão em Oklab)
  7. MDN, <color> (tipo de dado CSS)