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:
- Sistema (sem valor armazenado): a interface segue o esquema de cores do sistema. O ideal é que esse seja o padrão.
- Claro (armazenado como
"light"): a interface usa o tema claro, independente do esquema de cores do sistema. - 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
localStoragepode 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, usetry .. 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
storagedewindowpara 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:
- 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). - Substituição: armazenado literalmente como
lightoudark.
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:
- 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".
- Se o destino for diferente da preferência atual do sistema, armazene-o literalmente.
- 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.
- Defina o conteúdo de
<meta>como o valor armazenado oulight darkse 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:
- O usuário ativa ou desativa. O modo escuro é diferente da preferência do sistema, então
darké armazenado, e o site fica escuro. - A configuração do SO muda para escuro. O site permanece escuro (o valor armazenado agora corresponde à preferência do sistema, mas é mantido).
- A configuração do SO volta para o modo claro. O site permanece escuro.
- 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)"