Início
VA.
Voltar para Estudos

Seis variáveis, dois temas

Como manter tema claro e escuro sem duplicar uma linha de estilo, usando variáveis CSS e o Tailwind 4.

2 min de leitura

A forma mais comum de fazer dark mode com Tailwind é escrever dark: em todo lugar:

<div class="bg-white text-black dark:bg-neutral-950 dark:text-neutral-50">

Funciona, e escala mal. Cada componente novo repete a decisão, e mudar a cor de fundo do site vira um find-and-replace em cinquenta arquivos.

A inversão

O truque é fazer o componente falar sobre papel, não sobre cor. Não existe "branco" nem "cinza-950" — existe "fundo".

:root {
  --bg: #ffffff;
  --surface: #fafafa;
  --border: #e6e6e9;
  --fg: #0a0a0a;
  --muted: #6b6b73;
  --accent: #ff5c39;
}
 
.dark {
  --bg: #0a0a0a;
  --surface: #111113;
  --border: #232326;
  --fg: #fafafa;
  --muted: #8a8a93;
  --accent: #ff5c39;
}

Seis variáveis cobrem um site inteiro. Fundo, superfície elevada, borda, texto, texto secundário e um acento. Se você precisa de uma sétima, quase sempre é porque um componente está pedindo uma exceção que não deveria existir.

Ligando ao Tailwind 4

O Tailwind 4 dispensa tailwind.config.js — a configuração é CSS. O bloco @theme transforma cada variável numa família de utilitários:

@import "tailwindcss";
 
@custom-variant dark (&:where(.dark, .dark *));
 
@theme inline {
  --color-bg: var(--bg);
  --color-surface: var(--surface);
  --color-line: var(--border);
  --color-fg: var(--fg);
  --color-muted: var(--muted);
  --color-accent: var(--accent);
}

Depois disso, bg-bg, text-muted e border-line existem como classes normais e já respondem ao tema. O componente vira:

<div class="bg-bg text-fg border border-line">

Sem um único dark:.

O @custom-variant na segunda linha é obrigatório: por padrão o Tailwind 4 usa prefers-color-scheme, e um toggle manual precisa da estratégia de classe.

O flash na primeira pintura

Falta um problema real: o servidor não sabe qual tema o visitante escolheu. Se você renderizar claro e o usuário tiver escolhido escuro, ele vê um flash branco antes da hidratação.

O next-themes resolve injetando um script que roda antes da primeira pintura, lendo o localStorage e aplicando a classe no <html>:

<ThemeProvider
  attribute="class"
  defaultTheme="dark"
  enableSystem={false}
  disableTransitionOnChange
>

Duas consequências práticas. A primeira: o <html> precisa de suppressHydrationWarning, porque o script muda o atributo antes do React comparar as árvores. A segunda: qualquer componente que leia o tema — um botão que troca entre sol e lua, por exemplo — só pode renderizar o ícone depois de montar:

const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
 
return <button>{mounted ? (isDark ? <SunIcon /> : <MoonIcon />) : <span />}</button>;

Parece exagero para um ícone, mas é exatamente o mesmo motivo: o servidor não tem como acertar esse chute.

O teste que importa

Um bom teste de tema não é abrir os dois e achar bonito. É este: escolha uma cor nova e troque-a em um lugar só. Se você precisar tocar em mais de um arquivo, os componentes ainda estão falando de cor em vez de papel.

Tags

  • CSS
  • Tailwind
  • Design System