Modo noturno com CSS variables

Publicado em 16 jul 2020. Uns 5 minutos de leitura.

Quase todo sistema operacional hoje vem com suporte a definir um nível de "brilho" da interface, que geralmente tem dois valores: claro e escuro (light ou dark). As implementações modernas de CSS incluem suporte a verificar qual a preferência do usuário, se houver, e adaptar sua interface de acordo. Vamos implementar um modo noturno com CSS, usando CSS variables para deixar o código mais escalável e menos repetitivo!

A ideia é usar a media query chamada preferes-color-scheme. No caso do blog, defini que o modo padrão é o claro e que o modo escuro vai ser ativado só se o usuário definiu no sistema dele que quer esse modo. Para fins desse exemplo, vou usar uma div com id #root-1, mas nada impede de você adicionar as propriedades direto no html, como eu fiz na versão estática do código-fonte desse blog.

Vamos definir variáveis para nossas cores claras e escuras. Depois, vamos criar outras variáveis que vão usar as primeiras, mas ao invés de representarem uma cor, elas representam um conceito da interface, como o plano de fundo e o de frente. Por fim, usamos a media query para alterar estas variáveis caso o usuário use um tema escuro. No exemplo abaixo, você vai ver o resulado claro ou escuro, de acordo com a configuração do seu sistema.

#root-1 {
  /* Definindo as variáveis */
  --color-light: #e0e0ff;
  --color-dark: #050535;
  --icon-light: '☀️';
  --icon-dark: '🌙';

  /* Por padrão, usamos o tema claro */
  --color-background: var(--color-light);
  --color-foreground: var(--color-dark);
  --icon: var(--icon-light);

  color: var(--color-foreground);
  background-color: var(--color-background);
  padding: 1em;
  border-radius: 1em;
}

#root-1::before {
  content: var(--icon);
}

@media (prefers-color-scheme: dark) {
  #root-1 {
    /* Apenas muda as variáveis para o tema escuro */
    --color-background: var(--color-dark);
    --color-foreground: var(--color-light);
    --icon: var(--icon-dark);
  }
}
<div id="root-1">
  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
  Nulla quis augue ipsum. Aenean euismod sit amet urna sed
  pharetra. Curabitur felis turpis, tincidunt quis metus
  tincidunt, tempor molestie augue. Proin elementum viverra
  vestibulum.
</div>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla quis augue ipsum. Aenean euismod sit amet urna sed pharetra. Curabitur felis turpis, tincidunt quis metus tincidunt, tempor molestie augue. Proin elementum viverra vestibulum.

Não tem muito mistério! Para dar um tchan, coloquei um pseudoelemento ::before, definindo o conteúdo dele também via CSS, com os emoji de sol e lua, de acordo com o modo selecionado. Para ver a mudança, vá nas configurações do seu sistema operacional e ative/desative o modo noturno. Eu sei que você está com preguiça de ir lá trocar, então eu fiz um GIF para provar que funciona mesmo.

Imagem mostrando o modo noturno no macOS sendo ativado e desativado, e o resultado do exemplo acima mudando de tema corretamente

Como o CSS está em função da media query, não há uma maneira direta para o usuário alterar o tema no site sem mexer na configuração do seu sistema operacional ou navegador. Uma opção (que eu uso no blog) é permitir o usuário trocar de tema manualmente. Nesse caso, é preciso persistir que o usuário fez a mudança e sobrescrever de alguma maneira o comportamento do CSS, ignorando o efeito da media query. Para isso, podemos usar JavaScript, adicionando uma classe no elemento raiz para determinar o tema. Como classes terão uma prioridade maior que a media query, esta será ignorada.

#root-2 {
  /* Definindo as variáveis */
  --color-light: #e0e0ff;
  --color-dark: #050535;
  --icon-light: '☀️';
  --icon-dark: '🌙';

  color: var(--color-foreground);
  background-color: var(--color-background);
  padding: 1em;
  border-radius: 1em;
  cursor: pointer;
}

#root-2::before {
  content: var(--icon);
}

#root-2, #root-2.light {
  /* Por padrão, usamos o tema claro */
  --color-background: var(--color-light);
  --color-foreground: var(--color-dark);
  --icon: var(--icon-light);
}

#root-2.dark {
  /* Apenas muda as variáveis para o tema escuro */
  --color-background: var(--color-dark);
  --color-foreground: var(--color-light);
  --icon: var(--icon-dark);
}

@media (prefers-color-scheme: dark) {
  #root-2 {
    /* Apenas muda as variáveis para o tema escuro */
    --color-background: var(--color-dark);
    --color-foreground: var(--color-light);
    --icon: var(--icon-dark);
  }
}
<div id="root-2">
  Clique aqui para mudar o tema
</div>
const root = document.querySelector('#root-2')

root.addEventListener('click', _ => {
  if (root.classList.contains('dark')) {
    root.classList.add('light')
    root.classList.remove('dark')
  } else {
    root.classList.add('dark')
    root.classList.remove('light')
  }
})
Clique aqui para mudar o tema

Note que, se o seu sistema está com tema escuro selecionado, você vai precisar clicar duas vezes no exemplo para trocar de tema, na primeira interação. Isso acontece pois, inicialmente, não há classe na raiz. Uma maneira de resolver isso é tendo botões separados para mudar o tema, e exibir apenas um deles de acordo com o tema atual.

Outro ponto é que não estamos persistindo a escolha do tema, então se o usuário recarregar ou mudar de página, ele vai voltar para o modo claro ou escuro, de acordo com o sistema operacional. Dá para resolver isso armazenando a preferência no local storage do naveador. É assim que eu faço nesse blog. Quando a pessoa entra pela primeira vez, o padrão vai ser o do sistema dela, através da media query. Se ela escolher trocar de tema, a opção vai ficar salva no local storage e vai ser usada em futuras visitas.

É isso! Estou experimentando com esse formato de tutoriais com exemplos no meio do texto. Naturalmente funciona melhor com ideias bem visuais de front-end, como essa. Até a próxima!