Cores no CSS: hex, rgb, hsl e oklch na prática
Como escrever cor em CSS, o que muda entre hsl e oklch quando você clareia um tom e como montar uma paleta que não desbota no meio da escala.
Cor no CSS é sempre a mesma cor escrita de jeitos diferentes: #1b7f9d,
rgb(27 127 157) e hsl(194 71% 36%) pintam exatamente o mesmo pixel. O que
muda entre as notações é o quanto você consegue prever o resultado quando
mexe num número — e é nisso que oklch() deixa as outras para trás.
Todos os exemplos saem da mesma tela: o painel operacional do Metrô de Guaraciaba, com doze linhas, cada uma com a sua cor, e os avisos de status (operando, lentidão, interrompida). Nesse tipo de painel a cor não é enfeite: ela carrega informação, e precisa carregar a mesma quantidade de informação nas doze linhas.
Para conferir cada afirmação daqui eu rodei código no Node 24.16.0, com a
biblioteca colorjs.io 0.6.0, o jsdom (que normaliza cor do mesmo jeito que
o navegador) e o lightningcss (que resolve color-mix() na compilação). Toda
saída abaixo do código é a saída real desses programas.
Hexadecimal, rgb() e a sintaxe moderna sem vírgula
Um valor de cor no CSS é sempre uma combinação de vermelho, verde e azul, mais
uma opacidade opcional. O hexadecimal é essa combinação escrita em base 16, dois
dígitos por canal: #1b7f9d é 1b de vermelho, 7f de verde e 9d de azul.
Quando os dois dígitos de cada canal são iguais, dá para abreviar em três:
#0af é o mesmo que #00aaff.
.linha-8 { color: #1b7f9d; } /* hexadecimal */
.linha-8 { color: rgb(27 127 157); } /* sintaxe moderna, sem vírgula */
.linha-8 { color: hsl(194 71% 36%); }
.aviso { color: teal; } /* uma das 148 cores com nome */Repare que a sintaxe moderna separa os valores por espaço, não por vírgula.
As duas formas funcionam, mas a moderna é a que aceita a barra do canal alfa
(rgb(27 127 157 / 40%)) e a que as funções novas usam. Para provar que tudo
isso é a mesma cor, dá para pedir o valor calculado — que é o que o navegador
guarda depois de interpretar o que você escreveu.
import { JSDOM } from 'jsdom';
const escritas = [
'#1b7f9d',
'#1B7F9D',
'rgb(27, 127, 157)',
'rgb(27 127 157)',
'hsl(194 71% 36%)',
'#0af',
'#00aaff',
'teal',
'rebeccapurple',
];
const dom = new JSDOM(
escritas.map((c, i) => `<span id="c${i}" style="color: ${c}"></span>`).join('\n'),
);
const { document, getComputedStyle } = dom.window;
escritas.forEach((escrita, i) => {
const el = document.getElementById(`c${i}`);
console.log(escrita.padEnd(20), '->', getComputedStyle(el).color);
});Cinco notações diferentes, um único rgb(27, 127, 157). Maiúscula ou minúscula
no hex não muda nada. Nome de cor também é só um apelido para um trio de
números — e rebeccapurple existe mesmo, é uma homenagem gravada na
especificação.
O problema do hex aparece quando você precisa de uma variação dele. Para
clarear #1b7f9d em 20% você não tem por onde começar: os três números não
falam de claridade, falam de quanto de cada luz acender.
hsl(): matiz, saturação e luminosidade em números que dá para ler
hsl() reorganiza a mesma informação em três eixos que uma pessoa consegue
raciocinar. Matiz (hue) é o ângulo na roda de cores, de 0 a 360: 0 é
vermelho, 120 é verde, 240 é azul. Saturação é quanto de cor tem ali, de 0%
(cinza) a 100%. Luminosidade é a posição entre o preto (0%) e o branco
(100%).
.linha-8 { color: hsl(194 71% 36%); } /* a cor institucional */
.linha-8--claro { color: hsl(194 71% 90%); } /* só o L subiu */
.linha-8--escuro { color: hsl(194 71% 20%); } /* só o L desceu */
.linha-8--apagado{ color: hsl(194 12% 36%); } /* só o S desceu */
.linha-1 { color: hsl(14 71% 36%); } /* só o matiz girou */Cada linha ali muda um eixo por vez, e é isso que faz o hsl() ser tão mais
confortável do que o hex. Convertendo de volta:
import Color from 'colorjs.io';
const hex = (s) => new Color(s).to('srgb').toString({ format: 'hex' });
const variacoes = [
'hsl(194 71% 36%)',
'hsl(194 71% 90%)',
'hsl(194 71% 20%)',
'hsl(194 12% 36%)',
'hsl(14 71% 36%)',
];
for (const v of variacoes) console.log(v.padEnd(20), '->', hex(v));Cinco hexadecimais que você jamais adivinharia de cabeça saíram de cinco linhas
legíveis. É por isso que hsl() dominou os pré-processadores e os primeiros
design systems.
O defeito do hsl: mesmo L, brilhos completamente diferentes
Aqui começa o problema que este artigo existe para mostrar. O L do hsl()
não é o brilho que o olho enxerga. Ele é uma conta geométrica sobre os
canais RGB, e essa conta ignora o fato de que o olho humano é muito mais
sensível ao verde do que ao azul.
Consequência prática: se você pintar as doze linhas do metrô com
hsl(h 80% 50%), girando só o matiz, vai jurar que escolheu doze cores
igualmente fortes. Não escolheu. Dá para medir isso com a luminância
relativa da WCAG — o mesmo número que a fórmula de contraste usa.
import Color from 'colorjs.io';
const matizes = [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330];
// Luminância relativa da WCAG: o Y do XYZ-D65, que é o que a fórmula de
// contraste usa. Contraste contra o texto branco = (1 + 0.05) / (Y + 0.05).
const brilho = (cor) => new Color(cor).luminance;
const contrasteNoBranco = (cor) => 1.05 / (brilho(cor) + 0.05);
console.log('linha matiz | hsl(h 80% 50%) | oklch(0.65 0.10 h)');
console.log(' | hex Y contr. | hex Y contr.');
matizes.forEach((h, i) => {
const a = `hsl(${h} 80% 50%)`;
const b = `oklch(0.65 0.10 ${h})`;
console.log(
String(i + 1).padStart(5),
String(h).padStart(5),
'|',
new Color(a).to('srgb').toString({ format: 'hex' }),
brilho(a).toFixed(3).padStart(6),
(contrasteNoBranco(a).toFixed(2) + ':1').padStart(7),
'|',
new Color(b).to('srgb').toString({ format: 'hex' }),
brilho(b).toFixed(3).padStart(6),
(contrasteNoBranco(b).toFixed(2) + ':1').padStart(7),
);
});
const faixa = (escreve) => {
const ys = matizes.map((h) => brilho(escreve(h)));
const cs = matizes.map((h) => contrasteNoBranco(escreve(h)));
return `Y ${Math.min(...ys).toFixed(2)} – ${Math.max(...ys).toFixed(2)}` +
` contraste ${Math.min(...cs).toFixed(2)} – ${Math.max(...cs).toFixed(2)}`;
};
console.log('');
console.log('hsl ', faixa((h) => `hsl(${h} 80% 50%)`));
console.log('oklch ', faixa((h) => `oklch(0.65 0.10 ${h})`));hsl Y 0.07 – 0.73 contraste 1.34 – 9.04 oklch Y 0.26 – 0.29 contraste 3.08 – 3.40
Leia a coluna Y do lado do hsl. A linha 3, amarela, tem brilho 0.731. A
linha 9, azul, tem 0.066. As duas foram escritas com o mesmo 50% de
luminosidade, e uma reflete onze vezes mais luz que a outra.
Agora leia o contraste. Se você puser texto branco em cima do crachá de cada
linha, o azul dá 9.04:1 (excelente) e o amarelo dá 1.34:1 — praticamente
invisível. Ninguém mexeu no L. O L mentiu.
Do lado do oklch, as mesmas doze voltas na roda produzem Y entre 0.26 e
0.29, e contraste entre 3.08:1 e 3.40:1. É uma faixa que cabe numa decisão de
design; a do hsl não cabe em nada.
oklch(): a luminosidade que combina com o que o olho vê
oklch() tem três eixos com nomes parecidos com os do hsl(), mas construídos
em cima de um espaço de cor calibrado com testes de percepção humana. A ordem é
L, C, H:
| eixo | o que é | faixa | o que acontece se você mexer |
|---|---|---|---|
L |
luminosidade percebida | 0 a 1 (ou 0% a 100%) |
a cor clareia ou escurece de verdade, em qualquer matiz |
C |
croma, a “força” da cor | 0 até 0.322 no sRGB |
de cinza até o mais saturado que a tela alcança |
H |
matiz, em graus | 0 a 360 |
gira na roda, sem mexer na luminosidade percebida |
Um detalhe que economiza horas de confusão: o H do oklch() não é o mesmo
número que o H do hsl(). Os dois medem ângulo em graus, mas em rodas
diferentes, então a mesma cor troca de endereço quando você muda de modelo.
import Color from 'colorjs.io';
const azul = new Color('#1b7f9d'); // a cor institucional da linha 8
console.log('mesma cor:', azul.toString({ format: 'hex' }));
console.log(' matiz em hsl ', azul.to('hsl').coords[0].toFixed(1) + '°');
console.log(' matiz em oklch', azul.to('oklch').coords[2].toFixed(1) + '°');Trinta graus de diferença para o mesmo pixel. É por isso que o azul da linha 8
aparece como 194 enquanto o artigo está falando de hsl() e como 224 depois
que ele passa para oklch(): é a mesma tinta, medida por duas réguas. Copiar o
número de matiz de um modelo para o outro é o erro que faz a paleta inteira
girar de cor.
O croma não tem um teto fixo como a saturação do hsl(), e isso incomoda no
começo. Ele é aberto de propósito: o máximo possível depende do L, do H e do
que a tela consegue mostrar — o campeão do sRGB é o magenta #ff00ff, com
0.322. Na prática, 0.02 é quase cinza, 0.10 é uma cor de interface
confortável e acima de 0.20 você já está batendo no teto do monitor comum.
/* a grade de medição: doze matizes de 30 em 30 graus, mesmo L e mesmo C.
Quatro delas, com o hex que a tabela acima mediu: */
:root {
--linha-1: oklch(0.65 0.10 0); /* #c1758d */
--linha-3: oklch(0.65 0.10 60); /* #bc804d */
--linha-8: oklch(0.65 0.10 210); /* #2f9fb1 */
--linha-12: oklch(0.65 0.10 330); /* #b278ac */
}Repare que as quatro saem discretas, quase pastel. Não é defeito: é o preço de
igualar o brilho. Um amarelo berrante e um azul-marinho nunca vão ter o mesmo
peso visual, e o oklch() mostra isso na cara em vez de esconder. Se você
quiser crachás mais vibrantes, suba o C até onde o sRGB aguenta — a última
seção mede exatamente onde é esse limite.
Se você tentar o mesmo com hsl(), o “degrau 500” da sua paleta vira uma
coincidência diferente em cada matiz:
import Color from 'colorjs.io';
const linhas = [
{ nome: 'linha 3', hsl: 60, oklch: 60 },
{ nome: 'linha 8', hsl: 210, oklch: 210 },
{ nome: 'linha 12', hsl: 330, oklch: 330 },
];
const degraus = [
{ nome: 100, hslL: 88, okL: 0.92 },
{ nome: 300, hslL: 70, okL: 0.79 },
{ nome: 500, hslL: 52, okL: 0.65 },
{ nome: 700, hslL: 34, okL: 0.5 },
{ nome: 900, hslL: 16, okL: 0.35 },
];
const y = (c) => new Color(c).luminance;
for (const modelo of ['hsl', 'oklch']) {
console.log(modelo.toUpperCase());
for (const d of degraus) {
const ys = linhas.map((l) =>
modelo === 'hsl'
? y(`hsl(${l.hsl} 70% ${d.hslL}%)`)
: y(`oklch(${d.okL} 0.10 ${l.oklch})`),
);
console.log(
' degrau',
String(d.nome).padStart(3),
' Y:',
ys.map((v) => v.toFixed(3)).join(' '),
' diferença entre a mais clara e a mais escura:',
(Math.max(...ys) - Math.min(...ys)).toFixed(3),
);
}
console.log('');
}OKLCH degrau 100 Y: 0.763 0.804 0.749 diferença entre a mais clara e a mais escura: 0.056 degrau 300 Y: 0.481 0.512 0.471 diferença entre a mais clara e a mais escura: 0.041 degrau 500 Y: 0.266 0.287 0.259 diferença entre a mais clara e a mais escura: 0.028 degrau 700 Y: 0.120 0.132 0.116 diferença entre a mais clara e a mais escura: 0.016 degrau 900 Y: 0.040 0.046 0.038 diferença entre a mais clara e a mais escura: 0.008
No degrau 500, o amarelo e o rosa do hsl estão a 0.468 de distância de
brilho. Em oklch, 0.028 — quase dezessete vezes menos. Um “500” em oklch é
um nível; um “500” em hsl é só um número que você escreveu.
Alfa, transparent e o currentColor que herda
Toda notação em forma de função — rgb(), hsl(), oklch() — aceita um quarto
valor depois de uma barra: a opacidade. 0 é invisível, 1 é opaco, e a
palavra transparent é apenas um apelido para preto com alfa zero. (No
hexadecimal não existe barra: o alfa entra como um par extra de dígitos no fim,
#1b7f9d80.)
.faixa-linha-8 {
color: oklch(0.53 0.098 224); /* a cor da linha 8 */
background: oklch(0.53 0.098 224 / 15%); /* a mesma, quase apagada */
border: 2px solid currentColor; /* pega o color acima */
box-shadow: 0 2px 8px rgb(0 0 0 / 0.12);
}Cuidado com uma armadilha: cor com alfa não é uma cor, é uma instrução de mistura. O resultado depende do que estiver atrás. A mesma linha do painel dá dois hexadecimais diferentes no modo claro e no modo escuro:
import Color from 'colorjs.io';
// Composição alfa: resultado = frente * alfa + fundo * (1 - alfa), em sRGB.
const sobre = (frente, fundo, alfa) => {
const f = new Color(frente).to('srgb').coords;
const b = new Color(fundo).to('srgb').coords;
return new Color('srgb', f.map((v, i) => v * alfa + b[i] * (1 - alfa)))
.toString({ format: 'hex' });
};
const marca = '#077796';
for (const alfa of [0.08, 0.15, 0.3]) {
console.log(
'alfa', alfa.toFixed(2),
' sobre o papel branco:', sobre(marca, '#ffffff', alfa),
' sobre o fundo do modo escuro:', sobre(marca, '#151a1d', alfa),
);
}Com 15% de alfa, o mesmo azul vira #daebef no claro e #13282f no escuro. É
exatamente o comportamento que você quer numa faixa de fundo, e exatamente o que
você não quer num texto: se o fundo mudar, o contraste do texto muda junto,
sem aviso.
Já currentColor é outra história: ele não mistura nada, só copia o valor de
color do próprio elemento. E como color é uma propriedade herdada, o valor
desce a árvore inteira sozinho.
import { JSDOM } from 'jsdom';
const dom = new JSDOM(`
<div id="cartao" style="color: #077796">
<strong id="titulo">Linha 8 — Universidade</strong>
<svg id="icone"><path/></svg>
</div>
<div id="alerta" style="color: #9d391b">
<strong id="titulo2">Linha 3 — Feira</strong>
</div>
`);
const { document, getComputedStyle } = dom.window;
for (const id of ['cartao', 'titulo', 'icone', 'alerta', 'titulo2']) {
console.log(id.padEnd(8), '->', getComputedStyle(document.getElementById(id)).color);
}O ícone SVG e o título pegaram a cor sem que ninguém escrevesse color neles.
Um componente de cartão que usa currentColor na borda, no ícone e no traço
funciona nas doze linhas trocando uma declaração. Se a mecânica da herança
ainda parece mágica, vale revisar
o que o filho recebe do pai no CSS.
Uma escala de 50 a 900 girando só o L
Com oklch(), uma paleta de interface vira uma escada: fixe o matiz, fixe (mais
ou menos) o croma e desça o L. O croma precisa afinar nas pontas, porque perto
do branco e do preto a tela simplesmente não tem cor forte para dar.
import Color from 'colorjs.io';
const H = 224; // o azul-petróleo institucional do metrô
const degraus = [
[50, 0.97, 0.018],
[100, 0.93, 0.045],
[200, 0.87, 0.075],
[300, 0.79, 0.1],
[400, 0.7, 0.115],
[500, 0.62, 0.115],
[600, 0.53, 0.098],
[700, 0.44, 0.08],
[800, 0.35, 0.064],
[900, 0.27, 0.049],
];
const y = (c) => new Color(c).luminance;
const contraste = (a, b) => {
const [alto, baixo] = [y(a), y(b)].sort((m, n) => n - m);
return (alto + 0.05) / (baixo + 0.05);
};
const cor50 = `oklch(${degraus[0][1]} ${degraus[0][2]} ${H})`;
console.log('degrau oklch hex cabe no sRGB contraste sobre o 50');
for (const [nome, L, C] of degraus) {
const cor = `oklch(${L} ${C} ${H})`;
console.log(
String(nome).padStart(5),
' ',
cor.padEnd(24),
new Color(cor).to('srgb').toString({ format: 'hex' }),
String(new Color(cor).inGamut('srgb')).padStart(9),
(contraste(cor, cor50).toFixed(2) + ':1').padStart(15),
);
}Essa tabela é o contrato da paleta, e ela responde perguntas de design sem discussão. Texto do degrau 600 sobre o fundo do degrau 50 dá 4.72:1 — passa no mínimo de 4.5:1 da WCAG para texto normal. O 500 dá 3.25:1 e não passa: serve para borda, para ícone grande, para faixa, nunca para um parágrafo.
Na folha de estilo, a escada vira variáveis CSS e o resto do projeto para de escrever cor na mão:
:root {
--azul-50: oklch(0.97 0.018 224);
--azul-100: oklch(0.93 0.045 224);
--azul-500: oklch(0.62 0.115 224);
--azul-600: oklch(0.53 0.098 224);
--azul-900: oklch(0.27 0.049 224);
}
.painel { background: var(--azul-50); color: var(--azul-900); }
.painel__aviso { background: var(--azul-600); color: white; }
.painel__borda { border-block-end: 1px solid var(--azul-100); }color-mix(): derivar hover, borda e estado desabilitado
Ter a escada resolve os tons principais. Os estados (:hover, :disabled, a
borda mais fraquinha) você deriva com color-mix(), que mistura duas cores
num espaço que você escolhe.
.aviso {
background: oklch(0.53 0.098 224);
}
.aviso:hover {
background: color-mix(in oklch, oklch(0.53 0.098 224), black 12%);
}
.aviso:disabled {
background: color-mix(in oklch, oklch(0.53 0.098 224), white 55%);
}
.aviso--contorno {
border-color: color-mix(in oklch, oklch(0.53 0.098 224), transparent 70%);
}
.mistura-srgb {
background: color-mix(in srgb, oklch(0.53 0.098 224), oklch(0.7 0.115 60) 50%);
}
.mistura-oklch {
background: color-mix(in oklch, oklch(0.53 0.098 224), oklch(0.7 0.115 60) 50%);
}O compilador consegue resolver essas contas na hora do build, o que é uma
maneira confortável de ver o resultado sem abrir o navegador. Com o bloco acima
salvo como painel.css, ao lado do script:
import { readFileSync } from 'node:fs';
import { transform } from 'lightningcss';
const { code } = transform({
filename: 'painel.css',
code: readFileSync('painel.css'),
minify: false,
targets: { chrome: 100 << 16 },
});
console.log(code.toString());.aviso:hover { background: #006480; background: lab(38.4504% -19.4991 -24.7206); }
.aviso:disabled { background: #9cc1cf; background: lab(75.6819% -10.1915 -11.534); }
.aviso–contorno { border-color: #0777964d; border-color: lab(45.8503% -20.0692 -24.8337 / .3); }
.mistura-srgb { background: #6d8273; }
.mistura-oklch { background: #5e9558; background: lab(56.6257% -28.409 26.1834); }
Três coisas para reparar. A primeira: o compilador escreveu a mesma propriedade
duas vezes, um hex e um lab(), para navegadores antigos aproveitarem o hex.
A segunda: misturar com transparent produziu #0777964d, um hex de oito
dígitos — os dois últimos são o alfa, 4d em base 16 é 77, ou seja, 30%.
A terceira é a mais importante. As duas últimas regras misturaram o azul da
linha 8 com o amarelo da linha 3, meio a meio, mudando só o espaço da mistura.
Em srgb saiu #6d8273, um cinza-esverdeado sujo; em oklch saiu #5e9558,
um verde de verdade. Medindo o croma das duas:
import Color from 'colorjs.io';
const croma = (hex) => new Color(hex).to('oklch').coords[1].toFixed(3);
console.log('color-mix(in srgb, ...) = #6d8273 croma =', croma('#6d8273'));
console.log('color-mix(in oklch, ...) = #5e9558 croma =', croma('#5e9558'));A mistura em srgb perdeu dois terços da cor pelo caminho. É o mesmo motivo do
degradê que fica cinza no meio: em srgb o caminho entre duas cores passa por
perto do eixo neutro. Escreva in oklch (ou in oklab) e o problema
desaparece.
Gamut: o que acontece quando a cor não cabe no sRGB
O croma do oklch() não tem teto, mas a sua tela tem. Quando você pede uma cor
que o monitor não consegue mostrar, o navegador precisa escolher a mais próxima
que ele consegue — e essa escolha mexe justamente no L que você tinha ajustado
com cuidado.
import Color from 'colorjs.io';
console.log('croma sRGB? P3? hex depois do corte L real depois do corte');
for (const C of [0.10, 0.15, 0.20, 0.25, 0.30]) {
const cor = new Color(`oklch(0.65 ${C.toFixed(2)} 150)`);
const cortada = cor.clone().to('srgb');
cortada.coords = cortada.coords.map((v) => Math.min(1, Math.max(0, v)));
console.log(
C.toFixed(2).padStart(5),
String(cor.inGamut('srgb')).padStart(6),
String(cor.inGamut('p3')).padStart(6),
' ' + cortada.toString({ format: 'hex' }).padEnd(20),
cortada.to('oklch').coords[0].toFixed(3),
);
}Até croma 0.15 o verde da linha 6 cabe no sRGB e o L sai exatamente 0.650.
A partir de 0.20 ele estoura: o corte zera o canal vermelho e o L real sobe
para 0.656, depois 0.670, depois 0.684. Você pediu mais cor e recebeu mais
brilho — e lá se foi a régua que você tinha montado.
Se você quer aproveitar a tela larga sem quebrar a comum, declare a versão segura primeiro e a versão vibrante dentro de uma media query de gamut:
:root {
--linha-6: oklch(0.65 0.15 150);
}
@media (color-gamut: p3) {
:root {
--linha-6: oklch(0.65 0.20 150);
}
}O erro mais comum: a vírgula que derruba a declaração inteira
Quando uma cor vem errada no CSS, quase nunca é a cor que está errada — é a sintaxe. E o CSS não avisa: uma declaração inválida é descartada em silêncio, e o elemento fica com o valor anterior. É por isso que tanta gente vê vermelho onde queria azul.
Os dois deslizes que mais aparecem: esquecer o % da luminosidade num hsl()
escrito com vírgula, e misturar a vírgula antiga com a barra moderna no rgb().
import { JSDOM } from 'jsdom';
const dom = new JSDOM(`
<span id="a" style="color: red; color: hsl(194, 71%, 36)"></span>
<span id="b" style="color: red; color: hsl(194, 71%, 36%)"></span>
<span id="c" style="color: red; color: rgb(7, 119, 150 / 40%)"></span>
<span id="d" style="color: red; color: rgb(7 119 150 / 40%)"></span>
<span id="e" style="color: red; color: hsl(194 71 36)"></span>
`);
const { document, getComputedStyle } = dom.window;
for (const id of ['a', 'b', 'c', 'd', 'e']) {
const el = document.getElementById(id);
console.log(
el.getAttribute('style').split('; ')[1].padEnd(34),
'->',
getComputedStyle(el).color,
);
}As linhas a e c continuaram vermelhas. Não houve erro no console, não houve
aviso: o navegador leu hsl(194, 71%, 36), não reconheceu, jogou fora e ficou
com o color: red de antes.
A regra para não cair nisso é simples: ou tudo com vírgula, ou tudo com
espaço, e as duas gramáticas não se misturam. A barra do alfa só existe na
forma com espaço. Repare na linha e: ela também escreve 36 sem % e mesmo
assim funciona, porque na forma com espaço o hsl() aceita número puro na
saturação e na luminosidade. Na forma com vírgula, que é a antiga, o % é
obrigatório — foi exatamente o que derrubou a linha a. Quando estiver na
dúvida, escreva o %: ele é válido nas duas. No DevTools, uma declaração
descartada aparece riscada — quando a sua cor “não funciona”, olhe primeiro se a
linha está riscada, e só depois desconfie de
especificidade.
O que vem depois
O caminho prático é este: escolha os matizes em oklch(), monte a escada de
L uma vez, guarde em custom properties e derive os estados com color-mix().
Hex fica para o arquivo da marca; hsl() fica para ler código antigo.
A próxima peça é aplicar essa paleta em superfície: fundo, cantos arredondados
e sombra, em background, border-radius e box-shadow.
E se você chegou aqui sem ter visto como funcionam rem, % e ch, volte uma
casa em unidades no CSS — cor e unidade são as duas
metades do mesmo assunto. O mapa completo está na
trilha de CSS.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
Dá para usar oklch() hoje sem escrever uma cor de reserva?
Então hexadecimal virou coisa do passado?
Qual a diferença entre oklch() e oklab()?
Como escolher os matizes de doze linhas sem elas se confundirem?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0, com colorjs.io 0.6.0, jsdom 30.0.1 e lightningcss 1.33.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — Tipo de dado <color> — developer.mozilla.org
- MDN — oklch() — developer.mozilla.org
- CSS Color Module Level 4 — w3.org
- WCAG 2.2 — Understanding Contrast (Minimum) — w3.org
- Björn Ottosson — Oklab, a perceptual color space — bottosson.github.io



