CSS color-mix() e cores relativas: tons, hover e transparência
O color-mix() mistura duas cores num espaço de cor que você escolhe, e a sintaxe de cores relativas permite editar um canal de uma cor existente, como em oklch(from var(--brand) calc(l - 0.1) c h). Os dois rodam no navegador, funcionam com variáveis CSS e substituem a maioria das funções de cor do Sass. Misture em oklab para tons e hover, em srgb para transparência.
7 min de leitura
Como o color-mix() funciona
O color-mix() recebe um espaço de cor e duas cores com porcentagens opcionais e devolve a cor entre elas. color-mix(in oklab, var(--brand) 90%, black) significa 90% da cor da marca e 10% de preto, misturados em Oklab. Com uma porcentagem só, a outra cor fica com o resto. Sem nenhuma, cada uma fica com 50%.
O navegador calcula o resultado em tempo de execução, então funciona com custom properties, currentColor e troca de tema. As funções do Sass rodam uma vez, no build, e não enxergam valores de var().
Se as duas porcentagens somarem menos de 100%, o resultado também fica parcialmente transparente: color-mix(in srgb, red 30%, blue 30%) é uma mistura meio a meio com 60% de opacidade. Mantenha a soma em 100%, a não ser que você queira esse efeito.
CSS
.button {
--brand: #2563eb;
background: var(--brand);
}
.button:hover {
background: color-mix(in oklab, var(--brand) 90%, black);
}
.badge {
background: color-mix(in oklab, var(--brand) 10%, white);
border-color: color-mix(in srgb, var(--brand) 30%, transparent);
}Em que espaço de cor misturar: srgb, oklab ou oklch
O espaço muda o resultado mais do que a maioria das pessoas imagina. Abaixo, o azul puro #0000ff misturado meio a meio com o amarelo puro #ffff00, calculado com a mesma matemática que o navegador usa.
Em srgb os canais dão média exata de 128, 128, 128: a mistura é #808080, um cinza com croma zero. O Oklab também mistura em linha reta, e o azul e o amarelo ficam em lados opostos dela, então eles se anulam quase por completo num azul acinzentado e apagado. O Oklch interpola o ângulo do matiz, pelo caminho mais curto de 264° a 110°, então o ponto médio é um azul-petróleo vivo, com matiz 187°, que mantém o croma das duas cores.
- Azul #0000ff#0000ff
- Amarelo #ffff00#ffff00
- em srgb: #808080#808080
- em oklab: #6cabc7#6cabc7
- em oklch: #00bdb0#00bdb0
| Espaço | Azul + amarelo | OKLCH L, C | Blue-600 + yellow-400 | OKLCH L, C |
|---|---|---|---|---|
| srgb | #808080 | 0.600, 0 | #909880 | 0.665, 0.035 |
| oklab | #6cabc7 | 0.710, 0.077 | #90a3ad | 0.703, 0.026 |
| oklch | #00bdb0 | 0.710, 0.262 | #00bd9e | 0.703, 0.194 |
O segundo par é #2563eb e #facc15 do Tailwind. Os resultados em oklch ficam fora do sRGB, então o croma aparece antes do gamut mapping e o hex é a cor mais próxima que uma tela sRGB consegue mostrar.
Duas coisas chamam a atenção. O srgb chega à luminosidade 0,600, enquanto o ponto médio perceptual entre o azul (0,452) e o amarelo (0,968) é 0,710: é daí que vem o meio escuro e barrento dos gradientes em srgb. E só o oklch mantém a mistura viva, ao custo de passar por um matiz que nenhuma das duas cores tem.
Regra prática: misture em oklab para tons claros, tons escuros e estados de hover, em que você fica num só matiz e quer passos de luminosidade uniformes. Misture em oklch quando as duas cores forem saturadas e você quiser um resultado saturado. Misture em srgb para transparência, porque é assim que o navegador compõe as camadas.
Receitas: hover, tons claros e variantes transparentes
Todos os valores abaixo usam #2563eb como --brand. Texto branco sobre ela tem razão de contraste de 5,17:1.
Misturar 10% de preto escurece mais em oklab do que em srgb: o texto branco sobe para 6,50:1, contra 6,11:1. Oklab e oklch dão o mesmo resultado quando a segunda cor é preto ou branco, porque elas não têm matiz para interpolar.
| Receita | CSS | Resultado | Contraste |
|---|---|---|---|
| Hover, 10% mais escuro | color-mix(in oklab, var(--brand) 90%, black) | #1f55cc | 6.50:1 com texto branco |
| Active, 20% mais escuro | color-mix(in oklab, var(--brand) 80%, black) | #1947ae | 8.22:1 com texto branco |
| Hover misturado em srgb | color-mix(in srgb, var(--brand) 90%, black) | #2159d3 | 6.11:1 com texto branco |
| Fundo em tom claro, 10% | color-mix(in oklab, var(--brand) 10%, white) | #e8f0ff | 4.51:1 com texto na cor da marca |
| Fundo em tom claro, 20% | color-mix(in oklab, var(--brand) 20%, white) | #d2e2ff | 3.95:1 com texto na cor da marca |
| Transparente, 20% | color-mix(in srgb, var(--brand) 20%, transparent) | rgba(37, 99, 235, 0.2) | Parece #d3e0fb sobre branco |
Cuidado com os tons claros. O texto na cor da marca tem 5,17:1 sobre branco, 4,51:1 sobre um tom claro de 10% e só 3,95:1 sobre um de 20%, o que reprova no WCAG AA para texto normal. Se você colocar texto na cor da marca sobre um tom claro dela, use o tom de hover no texto.
Misturar com transparent mantém a cor e reduz só o alpha, porque o navegador mistura valores pré-multiplicados. O resultado se adapta ao que está por trás: a mesma regra parece #d3e0fb sobre branco e #132651 sobre uma superfície escura #0f172a. Um tom claro misturado com branco continua pálido numa página escura, então, para componentes que aparecem nos dois temas, a versão transparente costuma ser a melhor escolha.
Sintaxe de cores relativas: edite um canal
A sintaxe de cores relativas pega uma cor existente, separa em canais e deixa você reescrever qualquer um deles com calc(). Em oklch(from var(--brand) l c h), as palavras-chave l, c e h guardam a luminosidade (de 0 a 1), o croma e o matiz de --brand.
Ao contrário do color-mix(), ela muda exatamente uma propriedade: baixa a luminosidade sem mexer no croma, gira o matiz sem mudar a luminosidade ou adiciona alpha a uma cor guardada em hex.
| Receita | CSS | Resultado a partir de #2563eb | Texto branco |
|---|---|---|---|
| Mais escura | oklch(from var(--brand) calc(l - 0.1) c h) | #0442c8 | 8.02:1 |
| Mais clara | oklch(from var(--brand) calc(l + 0.1) c h) | #4484ff | 3.51:1 |
| Metade do croma | oklch(from var(--brand) l calc(c * 0.5) h) | #4e6faf | 4.98:1 |
| Matiz +30° | oklch(from var(--brand) l c calc(h + 30)) | #7a49df | 5.46:1 |
| Complementar | oklch(from var(--brand) l c calc(h + 180)) | #956700 | 4.98:1 |
| Cinza, mesma luminosidade | oklch(from var(--brand) l 0 h) | #707070 | 4.95:1 |
| Alpha de 50% | oklch(from var(--brand) l c h / 0.5) | rgba(37, 99, 235, 0.5) | Depende do fundo |
CSS
:root {
--brand: #2563eb;
--brand-hover: oklch(from var(--brand) calc(l - 0.08) c h);
--brand-muted: oklch(from var(--brand) l calc(c * 0.5) h);
--brand-ring: oklch(from var(--brand) l c h / 0.4);
--accent: oklch(from var(--brand) l c calc(h + 30));
}Observe as quatro linhas que mantêm o l: mudar o croma ou o matiz move o contraste com texto branco só entre 4,95:1 e 5,46:1, contra 5,17:1 da cor original. É a luminosidade que define o contraste, então variações de matiz feitas assim mantêm mais ou menos a mesma acessibilidade. As cores mais clara e complementar ficam fora do sRGB com croma total, e o navegador as converte na cor mais próxima que consegue mostrar.
color-mix(), cores relativas e darken() do Sass
Use color-mix() quando você pensa em “um pouco de preto” ou “um pouco de transparência”. Use a sintaxe de cores relativas quando você pensa em canais: “a mesma cor, 0,1 mais escura”. As duas substituem a maioria das funções de cor do Sass e acompanham a troca de tema sem novo build.
| color-mix() | Sintaxe de cores relativas | Sass darken() | |
|---|---|---|---|
| Onde roda | No navegador | No navegador | No build |
| Funciona com var() e currentColor | Sim | Sim | Não |
| Modelo de cor | O espaço que você indicar: srgb, oklab, oklch, hsl e outros | Qualquer função de cor: oklch(), rgb(), hsl(), lab() | Só HSL |
| Ideal para | Tons claros e escuros, transparência, mistura de dois tokens | Mudar um canal: luminosidade, croma, matiz ou alpha | Código antigo com cores fixas |
| Tamanho do passo | Uma fração da distância até a segunda cor | Exato, nas unidades do canal | Pontos HSL fixos, desiguais para o olho |
| Suportado desde | Chrome 111, Safari 16.2, Firefox 113 | Chrome 119, Safari 18, Firefox 128 | Qualquer navegador, a saída é estática |
Por que o darken() do Sass dá passos desiguais
darken($color, 10%) subtrai 10 pontos da luminosidade HSL. A luminosidade HSL não é a luminosidade percebida, então a mudança visível depende do matiz. Veja darken(10%) ao lado de oklch(from … calc(l - 0.1) c h) em quatro cores comuns de interface, medido com ΔE OK, em que cerca de 0,02 é a menor diferença que dá para perceber.
| Cor | Sass darken(10%) | ΔE OK | OKLCH l − 0.1 | ΔE OK |
|---|---|---|---|---|
| #2563eb | #134cca | 0.078 | #0442c8 | 0.099 |
| #facc15 | #d7ad05 | 0.099 | #d9ab00 | 0.101 |
| #16a34a | #107636 | 0.136 | #00842b | 0.094 |
| #dc2626 | #b21d1d | 0.090 | #b70000 | 0.089 |
O mesmo darken(10%) muda o verde 1,7 vez mais que o azul. A versão OKLCH fica entre 0,089 e 0,101 para qualquer matiz; a pequena variação vem do gamut mapping, já que algumas cores mais escuras caem logo fora do sRGB. O próprio Sass recomenda color.adjust() ou color.scale() em vez de darken(), e em CSS puro você não precisa de nenhum dos dois.
Suporte nos navegadores e fallbacks
O color-mix() funciona no Chrome e no Edge 111, no Safari 16.2 e no Firefox 113, todos lançados em 2023. A sintaxe de cores relativas chegou no Chrome e no Edge 119 e no Firefox 128; o Safari lançou uma versão inicial no 16.4 e a sintaxe atual no Safari 18. Todos os navegadores atuais suportam as duas.
O truque comum de colocar um valor simples na linha anterior não funciona quando o novo valor contém var(). Uma declaração com var() é sempre aceita na análise, então o navegador descarta a sua linha de fallback e, se não conseguir calcular o color-mix(), volta para o valor herdado ou inicial. Em vez disso, coloque a versão moderna dentro de @supports.
CSS
.button:hover {
background: #1f55cc;
}
@supports (color: color-mix(in oklab, red, blue)) {
.button:hover {
background: color-mix(in oklab, var(--brand) 90%, black);
}
}
@supports (color: oklch(from red l c h)) {
.badge {
border-color: oklch(from var(--brand) l c h / 0.3);
}
}Perguntas e respostas
O que o color-mix() faz no CSS?
O color-mix() mistura duas cores num espaço de cor que você indica e devolve o resultado, por exemplo color-mix(in oklab, var(--brand) 90%, black) para uma cor 10% mais escura que a da marca. Ele roda no navegador, então funciona com variáveis CSS e currentColor.
Que espaço de cor usar no color-mix()?
Use oklab para tons claros, tons escuros e estados de hover, porque ele dá passos de luminosidade uniformes e mantém o matiz. Use oklch ao misturar duas cores saturadas quando quiser um resultado saturado, e srgb ao misturar com transparent. Misturar azul e amarelo em srgb dá o cinza chapado #808080, enquanto o oklch dá um azul-petróleo vivo.
Como escurecer uma cor no hover só com CSS?
Use background: color-mix(in oklab, var(--brand) 90%, black) ou background: oklch(from var(--brand) calc(l - 0.1) c h). Para #2563eb, o primeiro dá #1f55cc e o segundo #0442c8, elevando o contraste com texto branco de 5,17:1 para 6,50:1 e 8,02:1.
Como deixar transparente a cor de uma variável CSS?
Escreva color-mix(in srgb, var(--brand) 20%, transparent) ou oklch(from var(--brand) l c h / 0.2). Os dois funcionam quando --brand está guardada em hex e mantêm a cor intacta, reduzindo só a opacidade.
O color-mix() é suportado em todos os navegadores?
Sim, todos os navegadores atuais suportam: Chrome e Edge desde o 111, Safari desde o 16.2 e Firefox desde o 113. Para navegadores antigos, coloque primeiro uma cor estática e a versão com color-mix() dentro de @supports, porque uma linha com var() não consegue cair no fallback sozinha.
Qual a diferença entre color-mix() e a sintaxe de cores relativas?
O color-mix() mistura duas cores por porcentagem, o que serve para tons claros, tons escuros e transparência. A sintaxe de cores relativas, como oklch(from var(--brand) l c calc(h + 30)), reescreve canais individuais de uma cor, o que serve para passos exatos de luminosidade, mudanças de matiz e alpha.