Dark Mode com variáveis de luminosidade invertida
Já mostramos o jeito mais fácil de fazer dark mode com CSS, uma técnica que usa variáveis CSS para armazenar valores de cores e faz a diferenciação entre dark mode e light mode através de uma classe.
Neste artigo, abordamos uma técnica interessantíssima para fazer Dark Mode com variáveis de luminosidade invertidas, algo tão incrível que pode se tornar a maneira padrão para implementar dark mode em qualquer projeto Web.
É possível usar variáveis CSS para componentes de cores individuais para evitar a repetição das mesmas coordenadas de cores várias vezes ao longo de um tema.
Dá até para usar a mesma variável para vários componentes, por exemplo, com HSL Hue e Saturation:
:root { --primary-hs: 250 30%;}
h1 { color: hsl(var(--primary-hs) 30%);}
article { background: hsl(var(--primary-hs) 90%);}
article h2 { background: hsl(var(--primary-hs) 40%); color: white;}Aqui está uma página simples feita com essa técnica:
Ao contrário das variáveis de pré-processadores, é possível até mesmo substituir localmente a variável para ter blocos com uma cor de destaque diferente:
:root { --primary-hs: 250 30%; --secondary-hs: 190 40%;}
article { background: hsl(var(--primary-hs) 90%);}
article.alt { --primary-hs: var(--secondary-hs);}Como pode ser visto neste exemplo:
Dark Mode entra em jogo
Tudo isso é ótimo e elegante, até que dark mode entre em jogo.
A ideia de usar propriedades personalizadas para facilitar a adaptação de um tema ao modo escuro não é nova. No entanto, na maioria dos artigos pelas webs, a estratégia sugerida é criar várias propriedades personalizadas, uma para cada cor, e substituí-las através de media queries.
Não deixa de ser uma abordagem interessante, mas, mesmo nos designs mais disciplinados, nem todas as cores são variáveis CSS.
Freqüentemente, encontram-se cores declaradas inline. Isso significa que lidar com dark mode é desgastante o suficiente para que a maioria simplesmente deixe para depois…
A boa notícia é que isso pode ser mitigado ao usar Dark Mode com variáveis de luminosidade invertidas (HSL)!
Saiba, de antemão, que não é um processo ótimo, e você deve, eventualmente, ajustá-lo para criar um modo escuro adequado — já que fazer um tema escuro não se trata apenas de trocar cores —, mas é melhor do que nada e pode servir como uma excelente base.
Explicando a técnica
A ideia-base é usar propriedades personalizadas (variáveis CSS) para a luminosidade (lightness) das cores, ao invés da cor inteira. Então, no dark mode, substituir essas variáveis com 100% - lightness.
Geralmente, essa técnica produz cores claras para cores escuras, cores médias para cores médias e cores escuras para cores claras e ainda permite definir cores embutidas, em vez de forçar o uso de uma variável para cada cor.
O código do exemplo:
:root { --primary-hs: 250 30%; --secondary-hs: 190 40%;
--l-0: 0%; --l-30: 30%; --l-40: 40%; --l-50: 50%; --l-90: 90%; --l-100: 100%;}
@media (prefers-color-scheme: dark) { :root { --l-0: 100%; --l-30: 70%; --l-40: 60%; --l-90: 10%; --l-100: 0%; }}
body { background: hsl(0 0% var(--l-100)); color: hsl(0 0% var(--l-0));}
h1 { color: hsl(var(--primary-hs) var(--l-30));}
article { background: hsl(var(--primary-hs) var(--l-90));}
article h2 { background: hsl(var(--primary-hs) 40%); color: white;}
footer { color: hsl(0 0% var(--l-40));}A diferença entre o modo claro e escuro fica assim:

Light mode e dark mode, gerados “automaticamente”, lado-a-lado.
Observe que toda a informação sobre lightnesses foi substituída por variáveis de luminosidade — o que, na verdade, não precisa ser obrigatório e tão abrangente.
Por exemplo, os títulos dos artigos teriam uma aparência melhor e teriam melhor contraste se simplesmente fossem mantidos iguais:

Comparação do modo escuro com cada claridade se tornando uma variável versus uma abordagem mais refinada, com exceções feitas conforme necessário (neste caso, as cores de fundo e de texto para article > h2).
Essas são decisões fáceis de tomar enquanto se analisa o CSS, substituindo as porcentagens de luminosidade por variáveis e visualizando o resultado.
O problema com HSL
Mas por que os cabeçalhos dos artigos foram mais fáceis de ler com suas cores originais do que com a claridade invertida? A principal causa é que a Luminosidade de HSL não corresponde realmente ao que os nós, humanos, percebemos como luminosidade, e essa diferença pode produzir discrepâncias perceptivas acentuadas.
Esse é o grande problema com esta abordagem: ela assume que a luminosidade de HSL realmente significa algo, mas, como discutido neste artigo, não significa. Por exemplo, amarelo e azul têm a mesma luminosidade HSL de 50%… Além disso, nota-se que as cores escuras têm diferenças menores entre elas do que as cores claras, porque o HSL não é perceptivelmente uniforme.
Isso significa, então, que a técnica de Dark Mode com variáveis de luminosidade invertidas não é útil para nada além de um placeholder enquanto se desenvolve um dark mode real?
A resposta é: não. :)
Hoje, já é possível trabalhar com cores LCH diretamente nos navegadores. A primeira implementação saiu no Safari e, desde maio de 2023, lch() funciona em todos os principais navegadores: o MDN o marca como Baseline Widely available.
LCH é um espaço de cores muito melhor para essa técnica, já que sua luminosidade na verdade significa algo, não apenas em diferentes tonalidades da mesma cor, mas em diferentes matizes e cores.
Compare esses 2 degradês, o superior mostrando várias cores HSL, todas com leveza de 50%, e o inferior, várias cores LCH, todas com leveza de 50%:
Eis um screenshot com a diferença:

Perceba que, em HSL, algumas cores (como amarelo e ciano) são muito mais claras do que outras. No LCH, todas as cores com a mesma luminosidade têm… A mesma luminosidade.
Tenha em mente que o croma de LCH não corresponde realmente à luminosidade de HSL, então, embora esteja definido com o mesmo número, ele não corresponde à mesma coisa.
Mas, nesse caso, como essa a técnica de Dark Mode com variáveis de luminosidade invertidas funcionaria com cores LCH? Vamos experimentar!
Foi usada esta ferramenta para converter as cores HSL existentes para LCH; depois, ajustados os valores um pouco manualmente, já que as cores inicialmente convertidas não pareciam boas em todas as luminosidades LCH — observe que as cores HSL com as mesmas matiz e saturação podem ter matizes diferentes e chromas em LCH.
Esta é a aparência desta técnica com as cores LCH:
Um screenshot, caso você não tenha conseguido ver direto no navegador:

Light mode e dark mode, gerados “automaticamente” com com variáveis de luminosidade invertidas LCH.
Não apenas o dark mode fica muito melhor, mas, mesmo no light mode, as 2 cores alternativas parecem mais uniformes, pois têm a mesma luminosidade LCH.
Aqui está uma comparação dos 2 dark modes:

Comparação dos 2 dark modes gerados “automaticamente” via claridade HSL (esquerda) e claridade LCH (direita).
Quando este artigo foi publicado, era preciso fornecer um substituto por meio de @supports para os navegadores sem LCH. Hoje, com lch() em todos os principais navegadores, isso só faz sentido se o projeto ainda precisa atender versões antigas.
Automatizando a geração de variáveis de luminosidade
Se estiver usando um pré-processador que suporta loops, como Sass, você pode automatizar a geração dessas variáveis e torná-las ainda mais granulares, por exemplo, com steps de 5%:
:root { @for $i from 0 through 20 { --l-#{$i * 5}: #{$i * 5}%; }}
@media (prefers-color-scheme: dark) { :root { @for $i from 0 through 20 { --l-#{$i * 5}: #{100 - $i * 5}%; } }}Dá para fazer variáveis de luminosidade sem tanta repetição?
Através dos exemplos, houve muita repetição de valores, sendo preciso declarar, por exemplo, --l-40 como 40% e, em seguida, definindo-o para 60% no dark mode. Será que não é possível derivá-lo de alguma forma, subtraindo o valor que já se tem de 100%?
Aqueles com alguma experiência em programação podem arriscar algo como:
--l-40: calc(100% - var(--l-40));Entretanto, isso não funcionará. CSS não é uma linguagem imperativa; não possui etapas de cálculo, em qu as variáveis possuem valores diferentes antes e depois de cada etapa. Não existe tal conceito de tempo; todas as declarações que são aplicadas atualmente, precisam ser verdadeiras ao mesmo tempo.
É mais semelhante à avaliação reativa (reactive evaluation) de fórmulas de planilhas do que à computação em JS e outras linguagens de programação populares (existem linguagens de programação reativas de propósito geral, mas são menos conhecidas).
Então, declarações como a acima são consideradas ciclos: uma vez que --l-40 não pode se referir a si mesmo, isso é um erro, e --l-40 seria definido com seu valor inicial como um mecanismo de recuperação de erro (já que o CSS não pode lançar erros).
Mas, então, existe ou não uma maneira de evitar declarar variáveis de claridade 2 vezes, uma para o modo claro e outra para o modo escuro?
Existe. Mas, talvez, não seja tão recomendada. Isso torna o código mais complicado de ler e compreender, com poucos benefícios. Mas por uma questão de diversão intelectual, vamos ver como é.
Em vez de definir --l-40 para 40%, vamos defini-lo em termos de sua diferença de 50%, (i.e., -10%). Então, calc(50% + var(--l-40)) retorna 40% e calc(50% - var(--l-40)) retorna 60%, os 2 valores de que precisamos. Podemos, portanto, declarar uma variável que é -1 no dark mode e 1 no light mode e apenas multiplicar com isso.
Eis um subconjunto de como o código ficaria:
:root { --dm: 1;
/* Declaração de exemplo: */ --l-40: -10%;}
@media (prefers-color-scheme: dark) { :root { --dm: -1; }}
/* Examplo de uso: */
footer { color: hsl(0 0% calc(50% + var(--dm) * var(--l-40));
}Ficou mais claro porque não é tão recomendado? Torna o uso muito mais complicado e dificílimo de ser lido. É esse tipo de adesão obsessiva à DRY que, com o tempo, os programadores acabamos percebendo que é contraproducente.
Conclusão
Lembremo-nos que Dark Mode é somente uma opção estética, não um milagre da acessibilidade que muda vidas ou aumenta a produtividade, como muitos divulgam.
Independentemente disso, fato é que é preciso saber implementar modo escuro, especialmente nesses dias, em que grandes empresas e produtos estão aderindo à opção estética cada vez mais — em outras palavras, o assunto está “hypado”.
Hoje, a técnica de fazer dark mode com variáveis de luminosidade invertidas LCH funciona em todos os principais navegadores e, o quanto antes você souber técnicas variadas para implementar dark mode de maneira eficiente, melhor para você e para a Web.