O problema do toggle tri-estado
Você abre o site, e lá está: um seletor de tema com Claro, Escuro e Sistema. Parece caprichado. Parece que respeita a escolha do usuário. Mas, na real, ele está competindo com o que o visitante realmente quer fazer.
Pensa comigo: por que alguém cai na sua página? Quer ler um artigo, comprar algo, resolver um problema. O toggle de tema é tangencial a isso. Cada opção extra num elemento persistente de UI é uma micro-decisão que ninguém pediu. E a dissonância cognitiva aparece: "Espera, qual a diferença entre Escuro e Sistema? O Sistema segue meu SO? Qual tá ativo agora?"
A discussão levantada por Lea Verou no CSS-Tricks resume bem: um controle de dois estados consegue expressar os três — desde que você aplique o override na primeira vez que ele fizer diferença.

O padrão de dois estados na prática
A ideia é simples: comece seguindo o prefers-color-scheme do sistema e só grave um override no localStorage quando o usuário clicar no toggle. Sem botão "Sistema".
// theme.js — toggle de dois estados com fallback do sistema
const root = document.documentElement;
const stored = localStorage.getItem('theme'); // 'light' | 'dark' | null
// 1. Resolve o tema inicial: override salvo > preferência do sistema
const sistemaPrefereEscuro = window.matchMedia('(prefers-color-scheme: dark)').matches;
const temaInicial = stored ?? (sistemaPrefereEscuro ? 'dark' : 'light');
root.dataset.theme = temaInicial;
// 2. Mostra só a opção *oposta* na UI
function renderToggle() {
const atual = root.dataset.theme;
// Se está claro, oferece "Escuro"; se escuro, oferece "Claro".
document.getElementById('theme-toggle').textContent =
atual === 'dark' ? '☀️ Claro' : '🌙 Escuro';
}
// 3. No clique: inverte, persiste, re-renderiza
document.getElementById('theme-toggle').addEventListener('click', () => {
const proximo = root.dataset.theme === 'dark' ? 'light' : 'dark';
root.dataset.theme = proximo;
localStorage.setItem('theme', proximo); // override salvo
renderToggle();
});
// 4. Reage a mudanças do SO SÓ se não houver override
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (localStorage.getItem('theme')) return; // usuário já escolheu
root.dataset.theme = e.matches ? 'dark' : 'light';
renderToggle();
});
renderToggle();
/* style.css — variáveis CSS controladas pelo atributo data-theme */
:root[data-theme="light"] {
--bg: #ffffff;
--fg: #111111;
}
:root[data-theme="dark"] {
--bg: #111111;
--fg: #f5f5f5;
}
body {
background: var(--bg);
color: var(--fg);
transition: background 0.2s ease, color 0.2s ease;
}
O pulo do gato tá no passo 4: mudanças do sistema propagam só até o usuário fazer uma escolha. Depois disso, o localStorage manda. E o melhor: o visitante nunca vê um botão "Sistema", porque "Sistema" não é um estado que ele precisa gerenciar — é o fallback natural.

Quando três estados realmente valem a pena
Calma, não sai deletando todo toggle tri-estado do seu codebase. O padrão de dois estados vale para toggles persistentes e sempre visíveis — aqueles no header que brigam com o objetivo principal do visitante. Três estados fazem sentido em dois cenários específicos:
1. O toggle mora num painel de configurações
Se o controle de tema está enterrado numa página de Preferências — onde o usuário chegou de propósito — o custo cognitivo da terceira opção cai pra quase zero. Nesse contexto, radios explícitos de Claro/Escuro/Sistema são até mais claros que um flip esperto de dois estados.
2. Você oferece mais de dois esquemas de cor
Se o site entrega Solarized, Nord, Dracula e um modo de alto contraste junto com o padrão do sistema, "Sistema" vira uma quinta opção real em vez de uma terceira redundante. O padrão de dois estados assume exatamente dois esquemas.
Cuidado: o caso de borda do toggle de volta
O que acontece se o usuário começa em Sistema (Claro), vai pra Escuro e volta? Ele cai em Sistema de novo ou num override Claro? A resposta honesta: tanto faz. Como a Lea aponta, a divergência só fica observável quando o SO troca pra escuro depois — e, nesse momento, corrigir é um clique. Não superengenheire uma máquina de estados pra um controle que o visitante mal nota.
A alternativa real: nenhum toggle
Se o design do seu site segura bem nos dois esquemas, considere entregar zero toggles e confiar 100% no prefers-color-scheme. O navegador já expõe isso nas configurações do SO. Um toggle é uma feature pra manter, testar em vários browsers e explicar. Às vezes o melhor UX é aquele que você não constrói.

Resumo da ópera
Menos UI fazendo o mesmo que mais UI é quase sempre a escolha certa — principalmente pra controles que ficam entre o visitante e o objetivo dele. O padrão de dois estados te dá:
- Controle total do usuário (override persistido no
localStorage) - Preferência do sistema respeitada (via
prefers-color-scheme) - Um botão a menos poluindo sua navegação
Se você mantém uma biblioteca de componentes ou design system, coloque isso como padrão. Reserve toggles tri-estado pra páginas de configuração dedicadas e temas multi-esquema. E se você curte ver como tooling moderno de IA on-device está sendo plugado em apps mobile, o tutorial de ADK para Kotlin & Android é uma ótima próxima leitura — o mesmo princípio de "menos superfície, mais clareza" vale pra UIs de agentes. Pra times que deployam infra de inferência por trás dessas experiências, o guia do SageMaker HyperPod inference operator cobre o lado backend.
Próximos passos: audite o header do seu site atrás de controles redundantes, rode um passe rápido de acessibilidade no seu prefers-color-scheme (ele respeita prefers-contrast?) e confira se seu toggle é anunciado corretamente pra leitores de tela via aria-pressed.