Dark mode e theming: implementando temas sem duplicar CSS
Dark mode deixou de ser um luxo e virou expectativa. Usuários passam horas diante de telas e muitos preferem, ou precisam, de uma interface escura para reduzir o cansaço visual, economizar bateria em telas OLED ou simplesmente por gosto. O problema é como implementar temas do jeito certo. A abordagem ingênua, escrever o CSS duas vezes (uma para claro, outra para escuro), cria um pesadelo de manutenção onde cada mudança precisa ser feita em dois lugares e as inconsistências se multiplicam. Fazer theming bem é, antes de tudo, uma questão de arquitetura de CSS.
Este guia é sobre a mecânica do theming, não sobre governança de componentes (o papel de um design system em manter a biblioteca coerente é tema de outro post). Aqui o foco é concreto: como estruturar cores com custom properties e tokens semânticos, respeitar a preferência do sistema, garantir contraste no escuro, adaptar imagens e sombras, e persistir a escolha do usuário sem aquele flash desagradável de tema errado.
O erro de duplicar CSS
Antes da solução, vale entender por que a abordagem óbvia falha. Duplicar estilos para cada tema significa que a cor de um botão está definida em dois blocos diferentes. Quando o designer muda o tom da marca, alguém precisa lembrar de atualizar os dois, e inevitavelmente esquece um. Multiplique isso por centenas de elementos e dois, três ou mais temas, e o CSS vira uma teia impossível de manter coerente. A raiz do problema é que os valores de cor estão espalhados e repetidos, em vez de centralizados.
A solução moderna inverte isso: em vez de duplicar estilos, você define os valores uma vez, em variáveis, e troca apenas os valores entre temas. O CSS que usa as cores nunca muda; só muda o que as variáveis apontam.
CSS custom properties: a fundação
A peça técnica que torna tudo possível são as CSS custom properties (as variáveis nativas do CSS). Elas permitem declarar um valor uma vez e referenciá-lo em qualquer lugar. Para theming, a estratégia é definir as cores no elemento raiz e sobrescrevê-las conforme o tema ativo:
:root {
--cor-fundo: #ffffff;
--cor-texto: #1a1a1a;
--cor-superficie: #f4f4f5;
}
:root[data-theme="dark"] {
--cor-fundo: #0f0f10;
--cor-texto: #f4f4f5;
--cor-superficie: #1c1c1f;
}
/* o componente usa a variável, nunca a cor direta */
.card {
background: var(--cor-superficie);
color: var(--cor-texto);
}
O ponto genial é que .card não sabe nem se importa com o tema. Ele usa var(--cor-superficie), e o valor certo é resolvido conforme o data-theme no elemento raiz. Trocar de tema é trocar um atributo, e a interface inteira responde. Nenhuma regra de componente é duplicada.
Tokens semânticos: nomeie o papel, não a cor
Aqui está a distinção que separa um theming frágil de um robusto. É tentador nomear as variáveis pela cor: --azul, --cinza-claro. Mas isso quebra no dark mode, onde o "cinza-claro" precisa virar um cinza escuro, e aí o nome mente. A solução são os tokens semânticos: nomear a variável pelo seu papel, não pela sua aparência.
- Em vez de
--branco, use--cor-fundo. - Em vez de
--cinza-escuro, use--cor-texto. - Em vez de
--azul-500, use--cor-primaria.
O nome descreve a função ("o fundo da página"), e o valor muda por tema sem que o nome deixe de fazer sentido. No tema claro, --cor-fundo é branco; no escuro, é quase preto; o nome continua correto. A boa prática combina duas camadas: uma paleta de tokens primitivos com as cores cruas (--azul-500), e uma camada de tokens semânticos que aponta para eles conforme o tema. O componente sempre usa o semântico.
Respeite a preferência do sistema
O usuário já disse ao sistema operacional se prefere claro ou escuro. A interface deveria começar respeitando isso. O CSS oferece a media query prefers-color-scheme exatamente para ler essa preferência:
@media (prefers-color-scheme: dark) {
:root {
--cor-fundo: #0f0f10;
--cor-texto: #f4f4f5;
}
}
O padrão ideal combina a preferência do sistema com a escolha explícita: por padrão, siga o prefers-color-scheme; se o usuário escolher um tema manualmente na interface, essa escolha vence e é lembrada. Assim você honra a preferência automática sem tirar o controle de quem quer decidir.
Contraste no escuro: não é só inverter
Um erro comum é achar que dark mode é apenas trocar branco por preto e vice-versa. Não é. O tema escuro tem regras próprias de legibilidade e conforto:
- Evite preto puro e branco puro. Texto branco puro (
#fff) sobre fundo preto puro (#000) gera um contraste tão duro que causa halação (o texto parece vibrar). Prefira um quase-preto para o fundo e um quase-branco para o texto. - Mantenha o contraste acessível. As mesmas exigências de contraste mínimo entre texto e fundo valem no escuro. Um cinza claro demais sobre fundo escuro pode falhar tanto quanto no tema claro.
- Reduza a saturação das cores de destaque. Cores muito vivas "brilham" demais sobre fundo escuro e cansam. Versões um pouco menos saturadas funcionam melhor no escuro.
- Use elevação por luz, não por sombra. No tema claro, superfícies "acima" ganham sombra; no escuro, sombras somem no fundo. A convenção é indicar elevação clareando levemente a superfície.
Imagens, sombras e outros detalhes
Além das cores de interface, alguns elementos precisam de atenção especial na troca de tema:
| Elemento | Cuidado no dark mode |
|---|---|
| Sombras | Sombras escuras somem no fundo escuro; use clareamento de superfície para dar profundidade |
| Ilustrações e logos | Versões com fundo transparente e cores que funcionem nos dois temas, ou uma variante por tema |
| Fotos | Considere reduzir levemente o brilho de imagens muito claras para não ofuscar |
| Bordas | Bordas sutis ajudam a separar superfícies quando as sombras deixam de funcionar |
Uma técnica útil é usar a própria media query ou o atributo de tema para trocar a fonte de uma imagem quando necessário, garantindo que ilustrações com fundo branco não apareçam como um bloco brilhante no escuro.
Persista a escolha e evite o flash
O detalhe que separa uma implementação amadora de uma polida é o FART (flash of incorrect theme), aquele piscar em que a página carrega no tema errado por um instante antes de corrigir. Ele acontece quando a preferência do usuário é lida tarde demais, depois de a página já ter pintado no tema padrão.
A solução tem duas partes:
- Persistir a escolha do usuário (tipicamente no armazenamento local do navegador), para lembrá-la nas próximas visitas.
- Aplicar o tema o mais cedo possível, antes de a página renderizar, definindo o atributo
data-themeno elemento raiz logo no início do carregamento, com base na escolha salva ou na preferência do sistema. Aplicado cedo, o tema correto já é o primeiro a aparecer, sem flash.
Esse cuidado é pequeno em código e enorme em percepção de qualidade: uma interface que já abre no tema certo passa uma sensação de solidez que a que pisca nunca alcança.
Temas como valores, não como duplicação
Fazer dark mode e theming corretamente é, no fundo, uma questão de arquitetura: centralizar as decisões de cor em tokens semânticos, expostos como custom properties, e trocar apenas os valores entre temas. Com essa base, adicionar um novo tema (escuro, alto contraste, uma marca alternativa) deixa de ser reescrever CSS e passa a ser definir um novo conjunto de valores. Some a isso o respeito à preferência do sistema, o cuidado com contraste e elevação no escuro, a adaptação de imagens e sombras e a persistência sem flash, e você tem um theming que escala em vez de virar dívida técnica.
Na ConstructorTI, implementamos theming e dark mode de forma sustentável: tokens semânticos sobre custom properties, respeito ao prefers-color-scheme com escolha do usuário, contraste acessível nos dois temas e persistência sem flash de tema errado. Se o seu produto precisa de dark mode, de múltiplas marcas ou de um sistema de temas que não vire pesadelo de manutenção, fale com o nosso time e saia da conversa com um plano claro de arquitetura de theming.