Como implementar dark mode com CSS variables e sem JavaScript

1. Fundamentos: Por que CSS Variables e Sem JavaScript?

A implementação de dark mode com CSS Variables e sem JavaScript representa uma abordagem moderna que prioriza performance, acessibilidade e simplicidade. Diferente das soluções tradicionais que dependem de JavaScript para manipular classes no DOM ou armazenar preferências em localStorage, esta técnica utiliza recursos nativos do CSS para oferecer uma experiência instantânea e respeitosa com as preferências do usuário.

Performance e acessibilidade: O carregamento é instantâneo porque não há scripts bloqueando a renderização. A regra prefers-color-scheme respeita automaticamente a configuração do sistema operacional do usuário, eliminando a necessidade de perguntar ou adivinhar sua preferência.

Manutenibilidade: Com as variáveis centralizadas em :root e escopos específicos, alterar um tema inteiro requer modificar apenas alguns valores. Não há código JavaScript para depurar ou atualizar.

Comparação com abordagens tradicionais: Soluções com JavaScript frequentemente causam flash de conteúdo não estilizado (FOUC) durante o carregamento, enquanto a abordagem puramente CSS elimina completamente esse problema.

2. Configuração Inicial: Definindo a Paleta de Cores com :root

A base de qualquer sistema de temas com CSS Variables é uma paleta de cores bem estruturada no seletor :root. A nomenclatura semântica é crucial para facilitar a troca de temas e a manutenção do código.

:root {
  /* Cores de fundo e texto */
  --color-bg: #ffffff;
  --color-text: #1a1a1a;
  --color-text-secondary: #666666;

  /* Cores de destaque */
  --color-primary: #0066cc;
  --color-primary-hover: #0052a3;
  --color-primary-text: #ffffff;

  /* Cores de superfície */
  --color-surface: #f5f5f5;
  --color-surface-hover: #e8e8e8;
  --color-border: #d1d1d1;

  /* Cores de estado */
  --color-success: #28a745;
  --color-warning: #ffc107;
  --color-error: #dc3545;

  /* Sombras */
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.12);
  --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
}

Nomenclatura semântica vs. funcional: Prefira nomes como --color-bg em vez de --color-white. Isso permite que você mude o tema sem renomear variáveis. Valores padrão (fallback) garantem compatibilidade com navegadores legados:

body {
  background-color: var(--color-bg, #ffffff);
  color: var(--color-text, #1a1a1a);
}

3. Implementação do Dark Mode com prefers-color-scheme

A regra @media (prefers-color-scheme: dark) é o coração desta implementação. Ela detecta automaticamente a preferência do sistema operacional e aplica os estilos correspondentes.

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #121212;
    --color-text: #e0e0e0;
    --color-text-secondary: #a0a0a0;
    --color-primary: #4da6ff;
    --color-primary-hover: #66b3ff;
    --color-primary-text: #121212;
    --color-surface: #1e1e1e;
    --color-surface-hover: #2d2d2d;
    --color-border: #404040;
    --color-success: #2ecc71;
    --color-warning: #f39c12;
    --color-error: #e74c3c;
    --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
    --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
  }
}

Garantindo compatibilidade: Para navegadores que não suportam prefers-color-scheme, as variáveis definidas em :root fora da media query servem como fallback. Você pode também definir um tema claro explícito:

@media (prefers-color-scheme: no-preference) {
  :root {
    /* Tema claro padrão */
  }
}

4. Estratégias de Escopo: Temas por Componente ou Seção

Embora o prefers-color-scheme funcione globalmente, você pode querer temas específicos para componentes ou seções. Classes contextuais permitem override manual sem JavaScript:

/* Tema escuro forçado em um componente específico */
.card.dark-mode {
  --color-bg: #2d2d2d;
  --color-text: #e0e0e0;
  --color-border: #404040;
}

/* Tema claro forçado */
.card.light-mode {
  --color-bg: #ffffff;
  --color-text: #1a1a1a;
  --color-border: #d1d1d1;
}

/* Uso no componente */
.card {
  background-color: var(--color-bg);
  color: var(--color-text);
  border: 1px solid var(--color-border);
}

Exemplo prático: Um modal que deve ter tema escuro independentemente da preferência do sistema:

.modal.dark-mode {
  --color-bg: #1e1e1e;
  --color-text: #e0e0e0;
  --color-surface: #2d2d2d;
  background-color: var(--color-bg);
  color: var(--color-text);
}

.modal-header {
  background-color: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
}

5. Tratamento de Imagens e Mídia no Dark Mode

Imagens e ícones precisam de tratamento especial no dark mode para manter a legibilidade e a estética. Filtros CSS são uma solução elegante:

@media (prefers-color-scheme: dark) {
  /* Reduz brilho de imagens para evitar ofuscamento */
  img:not(.no-dark-mode) {
    filter: brightness(0.8) contrast(1.1);
  }

  /* Ajusta ícones SVG */
  .icon {
    filter: invert(1) hue-rotate(180deg);
  }

  /* Logotipos que não devem ser alterados */
  .logo.no-dark-mode {
    filter: none;
  }
}

Elemento picture para imagens específicas: Quando você precisa de imagens diferentes para cada tema:

<picture>
  <source srcset="logo-dark.svg" media="(prefers-color-scheme: dark)">
  <img src="logo-light.svg" alt="Logotipo" class="no-dark-mode">
</picture>

6. Transições Suaves e Experiência do Usuário

Transições suaves evitam mudanças bruscas e melhoram a experiência do usuário. Aplique transition nas propriedades que mudam com o tema:

/* Transição global para elementos que usam variáveis de tema */
* {
  transition: background-color 0.3s ease,
              color 0.3s ease,
              border-color 0.3s ease,
              box-shadow 0.3s ease;
}

/* Evita transição em elementos que não devem animar */
.no-transition {
  transition: none !important;
}

Prevenindo FOUC: Como não há JavaScript, não há risco de flash de conteúdo não estilizado. O tema é aplicado antes mesmo do primeiro paint.

Dica avançada com color-scheme: Esta propriedade estiliza scrollbars e inputs nativos:

@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;
  }
}

@media (prefers-color-scheme: light) {
  :root {
    color-scheme: light;
  }
}

7. Manutenção e Extensibilidade: Adicionando Novos Temas

A estrutura com variáveis facilita a adição de novos temas. Você pode combinar prefers-color-scheme com prefers-contrast para temas de alto contraste:

/* Tema escuro de alto contraste */
@media (prefers-color-scheme: dark) and (prefers-contrast: more) {
  :root {
    --color-bg: #000000;
    --color-text: #ffffff;
    --color-primary: #66b3ff;
    --color-border: #ffffff;
    --color-surface: #1a1a1a;
  }
}

/* Tema sépia para leitura */
@media (prefers-color-scheme: light) and (prefers-contrast: less) {
  :root {
    --color-bg: #f5f0e8;
    --color-text: #5c4a3a;
    --color-primary: #8b6914;
    --color-border: #d4c5a9;
  }
}

Checklist de acessibilidade WCAG:
- Contraste mínimo de 4.5:1 para texto normal
- Contraste de 3:1 para texto grande
- Testar em ambos os modos com ferramentas como axe DevTools
- Verificar foco visível em elementos interativos

Versionamento de variáveis: Mantenha um arquivo único com todas as variáveis e use comentários para documentar mudanças:

/* 
 * Tema v2.1 - 2024
 * Adicionado: --color-surface-hover
 * Modificado: --color-primary para melhor contraste no dark mode
 */

Conclusão

Implementar dark mode com CSS Variables e sem JavaScript não é apenas uma questão técnica, mas uma decisão de design que prioriza performance, acessibilidade e manutenibilidade. Esta abordagem respeita as preferências do usuário desde o primeiro carregamento, elimina dependências de scripts e oferece uma base sólida para escalar para múltiplos temas. Com as técnicas apresentadas — desde a estruturação de variáveis até o tratamento de mídia e transições suaves — você tem tudo o que precisa para implementar um sistema de temas completo e profissional.

Referências