Criando um Alternador de Temas com Variáveis CSS
Valores de cores hardcoded espalhados por uma stylesheet tornam a troca de temas dolorosa. Mude uma cor e você estará caçando por dezenas de declarações. As variáveis CSS resolvem isso de forma elegante — defina seus design tokens uma única vez e, em seguida, troque os temas alterando um único atributo no elemento raiz.
Este artigo apresenta a construção de um alternador de temas sustentável: um que respeita as preferências do sistema automaticamente, responde à escolha manual do usuário e persiste essa escolha entre sessões.
Principais Conclusões
- As CSS custom properties são dinâmicas e atualizam em tempo de execução, tornando-as design tokens ideais para temas.
- Um atributo
data-themeno elemento raiz fornece uma maneira limpa e escalável de alternar entre qualquer número de temas. - A media query
prefers-color-schemelida com os padrões do sistema, enquanto o JavaScript e olocalStoragepreservam as escolhas manuais do usuário. - Um pequeno script inline no
<head>evita o flash de tema incorreto no carregamento da página. - Incluir a propriedade
color-schemegarante que a UI nativa do navegador se adapte ao seu tema ativo.
Por Que as Variáveis CSS São a Base Certa para Temas
As CSS custom properties são dinâmicas. Ao contrário das variáveis Sass, que compilam para valores estáticos, as variáveis CSS vivem no navegador e podem ser atualizadas em tempo de execução — por media queries, por JavaScript ou por um elemento pai alterando seu estado.
Isso as torna design tokens ideais. Defina suas cores de superfície, cores de texto e cores de destaque como variáveis em :root e, em seguida, referencie essas variáveis em toda a sua stylesheet. Trocar temas se torna uma única mudança no DOM, em vez de uma troca de stylesheet.
Configurando Seus Tokens de Tema
Comece definindo seu tema claro como o padrão e, em seguida, declare uma variante escura usando um seletor de atributo data-theme:
:root {
color-scheme: light;
--color-bg: #ffffff;
--color-text: #1a1a1a;
--color-primary: #302ae6;
--color-surface: #f4f4f4;
}
[data-theme="dark"] {
color-scheme: dark;
--color-bg: #161625;
--color-text: #e1e1ff;
--color-primary: #9a97f3;
--color-surface: #1e1e30;
}
Vale a pena incluir a propriedade color-scheme. Ela informa ao navegador qual modo está ativo, de modo que os elementos nativos da UI — barras de rolagem, inputs de formulário, anéis de foco — se adaptem automaticamente ao seu tema.
Agora, use esses tokens em toda a sua stylesheet:
body {
background-color: var(--color-bg);
color: var(--color-text);
}
Respeitando as Preferências do Sistema com prefers-color-scheme
As preferências do sistema e a troca manual de tema resolvem problemas diferentes. A prefers-color-scheme cuida do caso automático — usuários que configuraram seu sistema operacional para o modo escuro e esperam que os sites sigam essa preferência:
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
color-scheme: dark;
--color-bg: #161625;
--color-text: #e1e1ff;
--color-primary: #9a97f3;
--color-surface: #1e1e30;
}
}
A proteção :not([data-theme="light"]) garante que a escolha explícita do usuário sobrescreva o padrão do sistema. Sem ela, a media query sobrescreveria a seleção manual.
Uma Nota sobre light-dark()
A função CSS light-dark() permite declarar ambos os valores de tema inline:
:root {
color-scheme: light dark;
--color-bg: light-dark(#ffffff, #161625);
}
É elegante para conjuntos de tokens mais simples e funciona bem quando você precisa apenas de dois temas. Para sistemas com mais de dois temas ou tokens que variam além de cor, a abordagem com seletor de atributo oferece mais controle.
Observe que light-dark() requer que color-scheme esteja definido com ambos os valores para que a função seja resolvida corretamente. O suporte em navegadores modernos é robusto, mas ainda relativamente recente (Chrome 123+, Safari 17.5+, Firefox 120+), portanto, confirme a compatibilidade do seu público antes de depender dela.
Discover how at OpenReplay.com.
Adicionando JavaScript para Controle Manual Persistente
A detecção de preferência do sistema é apenas CSS, mas lembrar a escolha manual do usuário requer JavaScript. Aqui está uma implementação mínima e pronta para produção:
const STORAGE_KEY = 'theme-preference';
const root = document.documentElement;
function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function getSavedTheme() {
try {
return localStorage.getItem(STORAGE_KEY);
} catch {
return null;
}
}
function applyTheme(theme) {
root.setAttribute('data-theme', theme);
try {
localStorage.setItem(STORAGE_KEY, theme);
} catch {
// Storage unavailable — theme still applies for this session
}
}
// On load: respect saved preference, fall back to system
applyTheme(getSavedTheme() || getSystemTheme());
// Wire up your toggle button
document.getElementById('theme-toggle').addEventListener('click', () => {
const current = root.getAttribute('data-theme');
applyTheme(current === 'dark' ? 'light' : 'dark');
});
// Sync with OS changes when no manual preference is set
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!getSavedTheme()) {
root.setAttribute('data-theme', e.matches ? 'dark' : 'light');
}
});
Os blocos try/catch em torno do localStorage tratam de modos de navegação privada e erros de cota de armazenamento de forma elegante — o tema ainda se aplica para a sessão atual mesmo que não possa ser salvo.
Evitando o Flash do Tema Errado
Se o seu JavaScript carregar depois que a página for renderizada, os usuários verão brevemente o tema padrão antes do tema correto ser aplicado. Corrija isso com um pequeno script inline bloqueante no <head>:
<script>
try {
const saved = localStorage.getItem('theme-preference');
if (saved) {
document.documentElement.setAttribute('data-theme', saved);
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-theme', 'dark');
}
} catch {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-theme', 'dark');
}
}
</script>
Isso é executado de forma síncrona antes que qualquer conteúdo seja renderizado, eliminando o flash. Coloque-o o mais cedo possível no <head>, antes de quaisquer stylesheets, para que o atributo já exista quando o CSS for aplicado pela primeira vez.
O Padrão Que Escala
As variáveis CSS oferecem uma separação limpa entre seus design tokens e os estilos dos seus componentes. Os componentes referenciam variáveis; os temas definem variáveis. Adicionar um terceiro tema — alto contraste, uma variante de marca, uma paleta sazonal — significa adicionar um novo bloco de seletor de atributo no CSS e uma nova opção na lógica do seu alternador. Nada mais muda.
Conclusão
O verdadeiro valor aqui não é apenas o modo escuro — é uma arquitetura de temas que permanece sustentável à medida que seu design system cresce. Ao tratar as variáveis CSS como design tokens, respeitar tanto as preferências do sistema quanto as do usuário e lidar com os casos de borda (falhas de armazenamento, flashes de renderização, mudanças no nível do SO), você constrói um alternador que parece nativo à plataforma e trivial de estender. O padrão é pequeno, o retorno se acumula com cada novo tema ou componente que você adiciona.
Perguntas Frequentes
Ambos funcionam, mas um atributo data-theme é geralmente mais limpo porque expressa um único valor em vez de uma lista de modificadores. As classes são mais adequadas para estados composicionais, onde múltiplos flags coexistem. Com temas, você normalmente tem um valor ativo por vez, o que combina mais naturalmente com a semântica de um seletor de atributo.
Esse flash acontece quando seu JavaScript é executado depois que o navegador já pintou a página usando estilos padrão. A correção é um pequeno script inline síncrono no head que lê a preferência salva do localStorage e define o atributo data-theme antes que qualquer renderização ocorra. Para visitantes de primeira vez, você também pode verificar prefers-color-scheme para que a renderização inicial corresponda imediatamente ao tema do sistema.
Sim. As variáveis CSS podem armazenar qualquer valor CSS válido, incluindo escalas de espaçamento, raios de borda, sombras, font stacks e durações de animação. Isso as torna úteis para temas de densidade (layouts compactos versus confortáveis), preferências de movimento ou até mesmo variantes de tipografia. Trate as variáveis como design tokens em sentido amplo, não apenas como referências de cor.
Funciona bem em navegadores modernos (Chrome 123+, Safari 17.5+, Firefox 120+), mas não possui suporte em versões mais antigas. Para projetos com público moderno, é uma escolha elegante. Para uma compatibilidade mais ampla, a abordagem com atributo data-theme permanece mais segura e também escala para mais de dois temas, algo que o light-dark() não consegue fazer por si só.
Truly understand users experience
See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data. . Check our GitHub repo and join the thousands of developers in our community..