Alternância do modo escuro

Um botão de alternância do modo escuro permite que os usuários substituam o esquema de cores do sistema em um site. A substituição é aplicada atualizando <meta name="color-scheme">: light dark significa "seguir a preferência do sistema", light ou dark fixam um esquema explícito. As cores (light-dark()), as cores do sistema e a interface nativa dependem da propriedade CSS color-scheme. Portanto, o botão de alternância funciona mantendo essa propriedade sincronizada com o <meta> (consulte Refletir a substituição no CSS).

Alternância persistente, controle de configurações ou nenhuma alternância?

A maioria dos sites não precisa de um botão de alternância de esquema de cores persistente na interface (cabeçalho, navegação, rodapé etc.). Siga a preferência do sistema por padrão e, se uma substituição manual for necessária, coloque o controle em uma superfície de configurações separada: uma página, um painel ou uma sobreposição de configurações.

Fixar um esquema no SO é uma intenção legítima, mas rara, e intenções raras pertencem à divulgação progressiva, não a um controle que fica visível o tempo todo.

As alternâncias persistentes são encontradas principalmente em sites voltados para desenvolvedores ou relacionados a eles (por exemplo, para designers), em que a troca de esquemas é uma tarefa muito mais frequente do que para o usuário da Web comum. NÃO adicione um só porque é um padrão comum em sites de desenvolvedores: o interesse do desenvolvedor nesse controle não é representativo dos usuários típicos.

Implementação

Primeiro, siga todas as etapas em Modo escuro para oferecer suporte aos dois esquemas, usando a preferência do sistema como padrão.

Modelo de dados

O modelo de dados para o botão de modo escuro geralmente inclui três estados:

  1. Sistema (sem valor armazenado): a interface segue o esquema de cores do sistema. O ideal é que esse seja o padrão.
  2. Claro (armazenado como "light"): a interface usa o tema claro, independente do esquema de cores do sistema.
  3. Escuro (armazenado como "dark"): a interface usa o tema escuro, independentemente do esquema de cores do sistema.

Esse estado geralmente é armazenado em localStorage e refletido em um elemento <meta name="color-scheme"> no cabeçalho do documento.

Refletir a substituição no CSS

O <meta> é apenas uma dica de apresentação: qualquer declaração color-scheme no CSS do autor a substitui. Como o modo escuro define color-scheme: light dark em :root, atualizar apenas o <meta> é uma operação nula, ou seja, o CSS vence. Mapeie o <meta> de volta para a propriedade para que a cascata o siga:

:root {
  color-scheme: light dark;

  &:has(> head > meta[name="color-scheme"][content="dark"]) {
    color-scheme: dark;
  }
  &:has(> head > meta[name="color-scheme"][content="light"]) {
    color-scheme: light;
  }
}

NÃO fixar no código color-scheme: light ou color-scheme: dark como o padrão raiz. A declaração base PRECISA permanecer light dark. O CSS NÃO PODE depender do JS: se o JS nunca for executado, o <meta> vai permanecer light dark e o site vai seguir a preferência do sistema, sem que nada seja corrompido.

Persistência e prevenção de FOUC

Mantenha a substituição em localStorage. Remova a entrada ao retornar ao padrão do sistema.

Para evitar que o esquema errado apareça, aplique o valor armazenado em um script inline (NÃO type=module, NÃO defer) colocado imediatamente após o elemento <meta>:

<meta name="color-scheme" content="light dark">
<script>
document.querySelector('meta[name="color-scheme"]').content = localStorage.getItem("color-scheme") ?? "light dark";
</script>
  • Este script inline existe apenas para evitar o FOUC. Mantenha-o o menor possível. O JS adicional para lidar com a alternância do esquema de cores pode ser carregado mais tarde.
  • Em casos raros, o acesso a localStorage pode gerar um erro (por exemplo, dados do site bloqueados), mas como esse script não faz mais nada, o único efeito colateral seria um erro no console. Se isso for importante, use try .. catch. Verifique se o tema ainda está aplicado à página, mesmo que não possa ser mantido.
  • A substituição pode mudar em outra guia: processe o evento storage de window para manter a sincronização.
  • NÃO use matchMedia() para remover o valor armazenado quando a preferência do sistema mudar para corresponder a ele. O SO de muitos usuários muda de esquema automaticamente com base na hora do dia. Remover o valor armazenado sempre que os dois coincidem impossibilitaria a fixação de um esquema.

Ramificação para valores HTML e não coloridos

As cores não precisam de trabalho extra: quando o <meta> é mapeado para color-scheme (acima), o light-dark() segue a substituição. Esta seção é sobre como adaptar outros valores (por exemplo, font-weight, fontes de mídia etc.).

Quando a página simplesmente segue a preferência do sistema, (prefers-color-scheme: dark) pode ser usado para ramificação, inclusive em fontes <picture> ou <video>. Substituir o esquema de cores adiciona uma complicação extra: a preferência do sistema deixa de ser a fonte de verdade, e não há uma alternativa direta para APIs que aceitam apenas consultas de mídia.

NÃO dependa exclusivamente de classes aplicadas por JS, como .dark, para ramificação, porque elas estarão incorretas se o JS não for carregado e o padrão do SO for escuro.

É possível combinar a consulta de mídia com um seletor :root:has(> head > meta[name="color-scheme"][content="dark"]) (ou light) para ramificar. Consulte Combinações de seletor e regra @ para saber como implementar isso de forma elegante.

As opções do controle também são ramificações sem cor: inclua os dois estados na marcação (ícone e texto de ação visualmente oculto, por exemplo, "Mudar para o tema escuro") e oculte o inativo usando esses seletores. O display: none também remove a opção redundante da TA, de modo que o controle não precisa de JS para se manter atualizado quando a preferência do sistema muda.

Para mídia em HTML, a única maneira no momento é incluir as duas versões como elementos separados e alternar a visibilidade de forma adequada. Ocultar elementos <source> com display: none não funciona.

Interface do usuário

Dois ou três estados?

Em todos os casos, a interface PRECISA permitir a reversão ou a definição do esquema de cores para o sistema.

Prefira exibir dois estados em alternâncias com restrição de espaço, como as persistentes no Chrome do site (cabeçalho, navegação, rodapé etc.), em que o espaço é limitado e o ajuste das configurações geralmente é ortogonal ao objetivo do usuário.

Um controle de esquema de cores em uma superfície de configurações separada (página, painel ou sobreposição) é um cenário diferente e PODE expor os três estados explicitamente ("Claro", "Escuro", "Sistema"): nela, o usuário já está tomando decisões deliberadas sobre o comportamento futuro, e há espaço para explicar as opções.

Três estados explícitos também são necessários se o site implementar esquemas que dependem do contexto (por exemplo, um modo claro mais escuro quando o SO está no modo escuro), já que "Claro" e "Sistema (atualmente claro)" são genuinamente diferentes.

Alternâncias de dois estados

Quando esse controle é usado, ele tende a ser um ajuste temporário de conforto ("está muito claro agora"), em vez de garantir que o estado atual seja preservado a longo prazo. Portanto, para alternâncias de dois estados, basta exibir duas das três configurações: sistema (para que a configuração possa ser revertida) e o oposto atual do sistema, armazenado como o valor literal atual.

Os únicos dois estados devem ser:

  1. Padrão do sistema: nenhum valor armazenado, conteúdo <meta> light dark. Exibido como o valor resolvido atual (por exemplo, um ícone de sol quando claro).
  2. Substituição: armazenado literalmente como light ou dark.

Basicamente, é um controle de três estados (light dark, light, dark) em que o estado explícito que corresponde à preferência atual do sistema não pode ser acessado.

Quando o usuário ativa:

  1. Esquema de destino = o oposto do esquema renderizado no momento (valor armazenado, se houver, ou preferência do sistema). A intenção do usuário é "selecionar o oposto do que estou vendo agora", NÃO "selecionar o inverso do padrão do sistema".
  2. Se o destino for diferente da preferência atual do sistema, armazene-o literalmente.
  3. Se o destino corresponder à preferência atual do sistema, o usuário estará desfazendo o ajuste: remova o valor armazenado. NÃO o armazene, porque isso fixaria o esquema de forma invisível contra futuras mudanças no sistema.
  4. Defina o conteúdo de <meta> como o valor armazenado ou light dark se não houver nenhum.

IMPORTANTE: a divergência precisa ser verificada apenas no momento do armazenamento, nunca retroativamente. Um valor armazenado que a preferência do sistema muda posteriormente para corresponder PRECISA ser mantido. O SO de muitos usuários muda de esquema automaticamente com base na hora do dia. Remover o valor armazenado sempre que os dois coincidem impossibilitaria a fixação de um esquema.

Exemplo de cenário, começando com o SO definido como claro:

  1. O usuário ativa ou desativa. O modo escuro é diferente da preferência do sistema, então dark é armazenado, e o site fica escuro.
  2. A configuração do SO muda para escuro. O site permanece escuro (o valor armazenado agora corresponde à preferência do sistema, mas é mantido).
  3. A configuração do SO volta para o modo claro. O site permanece escuro.
  4. O usuário ativa ou desativa. O destino (claro) corresponde à preferência do sistema, então o valor armazenado é removido e o site segue o sistema novamente.

Alternâncias de três estados

Quando há mais espaço e os usuários já estão pensando em definir preferências de longo prazo, um controle de três estados pode oferecer mais clareza. Nesse caso, o modelo de dados é mapeado de forma limpa para as três opções de interface: sistema, claro e escuro.

Para reduzir a carga cognitiva, a opção "Sistema" também precisa mostrar qual é a configuração atual do sistema, por exemplo:

  • Ícone, por exemplo, um ícone de sol no canto inferior direito do ícone do sistema
  • Texto, por exemplo, "Sistema (atualmente claro)"