Tutoriais

Tematização de Cores em React: Variáveis CSS e Context

10 min de leitura

A tematização é um daqueles problemas que parece simples até você ter que mantê-lo em escala. Um único toggle entre modo claro e escuro é simples. Um produto que atende a múltiplas marcas, oferece paletas personalizáveis pelo usuário ou precisa alternar temas instantaneamente sem recarregar a página requer uma arquitetura mais deliberada.

O modelo de componentes do React e as propriedades customizadas CSS são uma combinação natural para tematização. As variáveis CSS lidam com os valores de cor declarativamente; o React Context gerencia o estado do tema; e o Tailwind CSS (em projetos modernos) faz a ponte entre ambos. Este guia aborda cada camada — como estruturar a arquitetura do tema, implementar a base de variáveis CSS, conectar o estado React e lidar com cenários multi-marca.


Padrões de Arquitetura de Tema

Padrão 1: Somente Variáveis CSS (Sem Estado JavaScript)

O padrão mais simples para suporte básico ao modo escuro não requer nenhum estado React. Uma media query prefers-color-scheme muda os valores de propriedades customizadas, e cada componente atualiza automaticamente:

/* globals.css */
:root {
  --color-bg: #F8FAFC;
  --color-text: #1E293B;
  --color-brand: #2563EB;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #0F172A;
    --color-text: #F1F5F9;
    --color-brand: #60A5FA;
  }
}

Componentes usam var(--color-bg) e nunca precisam saber sobre o tema atual. O navegador cuida de tudo.

Quando usar: Sites onde você quer respeitar a preferência em nível de sistema operacional e não precisa de um toggle controlado pelo usuário.

Limitação: Não há como os usuários substituírem a configuração do sistema operacional dentro do aplicativo.

Padrão 2: Troca de Tema por Atributo de Dados

Adicione um atributo data-theme no elemento <html> para tornar o tema ativo explícito e substituível. Isso é compatível com SSR, evita flash do tema errado e é trivialmente combinado com persistência em localStorage:

/* Padrão: claro */
[data-theme="light"],
:root {
  --color-bg: #F8FAFC;
  --color-text: #1E293B;
  --color-brand: #2563EB;
  --color-surface: #FFFFFF;
  --color-border: #E2E8F0;
}

/* Escuro */
[data-theme="dark"] {
  --color-bg: #0F172A;
  --color-text: #F1F5F9;
  --color-brand: #60A5FA;
  --color-surface: #1E293B;
  --color-border: #334155;
}
// Aplicar tema antes da primeira renderização (script inline no <head>)
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);

Quando usar: A maioria dos aplicativos em produção que precisam de um toggle controlado pelo usuário. A abordagem de atributo de dados é o padrão atual da indústria — usada pela implementação de modo escuro do Tailwind CSS, Radix UI e pela maioria dos sistemas de design modernos.

Padrão 3: Troca de Tema Baseada em Classe

Semelhante ao atributo de dados, mas usa classes CSS. A configuração darkMode: 'class' do Tailwind depende desse padrão:

.dark {
  --color-bg: #0F172A;
  --color-text: #F1F5F9;
}

Adicionar a classe dark ao <html> ativa os valores de token escuros. O Tailwind então aplica utilitários com prefixo dark: quando essa classe está presente.

Quando usar: Projetos usando Tailwind CSS com a estratégia de modo escuro class.


Variáveis CSS para Cores: O Sistema de Tokens

Nomenclatura Semântica vs. Descritiva

A decisão mais importante em um sistema de tokens é a nomenclatura. Nomear pelo valor da cor (--blue-500, --gray-900) torna a variável fácil de entender isoladamente, mas impossível de tematizar — mudar --blue-500 para roxo quebra a semântica do nome.

Nomear pelo papel semântico (--color-brand, --color-text-muted) permite que os valores mudem completamente entre os temas enquanto os componentes permanecem corretos:

:root {
  /* ---- Primitivos de Cor (não usados diretamente em componentes) ---- */
  --blue-500: #3B82F6;
  --blue-700: #1D4ED8;
  --slate-50: #F8FAFC;
  --slate-800: #1E293B;
  --slate-900: #0F172A;

  /* ---- Tokens Semânticos (usados em componentes) ---- */
  /* Texto */
  --text-primary: var(--slate-800);
  --text-secondary: #64748B;
  --text-disabled: #94A3B8;
  --text-inverse: var(--slate-50);

  /* Superfícies */
  --bg-base: var(--slate-50);
  --bg-elevated: #FFFFFF;
  --bg-sunken: #F1F5F9;

  /* Marca */
  --brand: var(--blue-700);
  --brand-hover: #1E40AF;
  --brand-subtle: #EFF6FF;
  --on-brand: #FFFFFF;

  /* Feedback */
  --error: #DC2626;
  --error-bg: #FEF2F2;
  --success: #16A34A;
  --success-bg: #F0FDF4;
  --warning: #D97706;
  --warning-bg: #FFFBEB;
}

[data-theme="dark"] {
  --text-primary: #F1F5F9;
  --text-secondary: #94A3B8;
  --text-disabled: #475569;
  --text-inverse: #0F172A;

  --bg-base: #0F172A;
  --bg-elevated: #1E293B;
  --bg-sunken: #0D1426;

  --brand: #60A5FA;
  --brand-hover: #93C5FD;
  --brand-subtle: #172554;
  --on-brand: #0F172A;

  --error: #F87171;
  --error-bg: #1C0A0A;
  --success: #4ADE80;
  --success-bg: #052E16;
  --warning: #FCD34D;
  --warning-bg: #1C1000;
}

O sistema de duas camadas (primitivos + tokens semânticos) oferece o melhor dos dois mundos: uma paleta de cores bruta para referência e tokens semânticos para uso em componentes.

Gerando Sua Escala de Cores

Antes de construir o sistema de tokens, você precisa da paleta de cores bruta. O Gerador de Tonalidades produz uma escala completa de 50–950 a partir de uma única cor de marca — o mesmo padrão de escala usado pelo Tailwind CSS. Insira o código hex da sua marca e obtenha o conjunto completo de variantes de escuro para claro prontas para usar como tokens primitivos.

Por exemplo, ao inserir #2563EB como seu azul de marca, são gerados:

  • blue-50: #EFF6FF
  • blue-100: #DBEAFE
  • blue-500: #3B82F6
  • blue-700: #1D4ED8
  • blue-900: #1E3A8A
  • blue-950: #172554

Esses valores primitivos então preenchem seus tokens semânticos, garantindo que as cores do modo escuro sejam harmonicamente relacionadas às do modo claro, em vez de aproximações escuras arbitrárias.


React Context para Estado de Tema

O Padrão ThemeContext

Para aplicativos com um toggle de tema controlado pelo usuário, o React Context fornece a camada de gerenciamento de estado. O contexto armazena o nome do tema ativo e expõe uma função de toggle:

// contexts/ThemeContext.tsx
import { createContext, useContext, useEffect, useState } from 'react';

type Theme = 'light' | 'dark' | 'system';

interface ThemeContextValue {
  theme: Theme;
  resolvedTheme: 'light' | 'dark'; // Tema efetivamente aplicado (resolve 'system')
  setTheme: (theme: Theme) => void;
}

const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setThemeState] = useState<Theme>(() => {
    if (typeof window === 'undefined') return 'system';
    return (localStorage.getItem('theme') as Theme) || 'system';
  });

  const systemPrefersDark = typeof window !== 'undefined'
    ? window.matchMedia('(prefers-color-scheme: dark)').matches
    : false;

  const resolvedTheme: 'light' | 'dark' =
    theme === 'system'
      ? (systemPrefersDark ? 'dark' : 'light')
      : theme;

  useEffect(() => {
    const root = document.documentElement;
    root.setAttribute('data-theme', resolvedTheme);
    localStorage.setItem('theme', theme);
  }, [theme, resolvedTheme]);

  // Ouvir mudanças de preferência do sistema quando theme === 'system'
  useEffect(() => {
    if (theme !== 'system') return;
    const media = window.matchMedia('(prefers-color-scheme: dark)');
    const handler = () => {
      document.documentElement.setAttribute(
        'data-theme',
        media.matches ? 'dark' : 'light'
      );
    };
    media.addEventListener('change', handler);
    return () => media.removeEventListener('change', handler);
  }, [theme]);

  const setTheme = (newTheme: Theme) => {
    setThemeState(newTheme);
  };

  return (
    <ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme(): ThemeContextValue {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
  return ctx;
}

O ThemeProvider envolve seu aplicativo (ou a subárvore relevante) e o hook useTheme expõe o estado do tema em qualquer lugar na árvore de componentes:

// components/ThemeToggle.tsx
import { useTheme } from '../contexts/ThemeContext';

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();

  return (
    <button
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
      aria-label={`Mudar para o modo ${theme === 'dark' ? 'claro' : 'escuro'}`}
      style={{
        background: 'var(--bg-elevated)',
        color: 'var(--text-primary)',
        border: '1px solid var(--color-border)',
        padding: '8px 16px',
        borderRadius: '8px',
        cursor: 'pointer',
      }}
    >
      {theme === 'dark' ? '☀ Claro' : '☾ Escuro'}
    </button>
  );
}

Os componentes usam propriedades customizadas CSS para seus estilos visuais — eles não precisam ler resolvedTheme para aplicar cores. O contexto só é necessário para UI que exibe ou controla o estado atual do tema.

Prevenindo Flash do Tema Errado (FOTWT)

Aplicativos renderizados no servidor enfrentam o problema de flash do tema errado: o servidor renderiza HTML sem saber a preferência de tema do usuário, o navegador exibe esse HTML momentaneamente, então o React hidrata e aplica o tema correto — causando um flash visível.

A solução é um script inline de bloqueio no <head> que lê localStorage e aplica o atributo de tema antes da página ser renderizada:

<!-- No seu <head>, antes de quaisquer links CSS -->
<script>
  (function() {
    try {
      var stored = localStorage.getItem('theme');
      var systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
      var theme = stored === 'dark' || stored === 'light'
        ? stored
        : (systemDark ? 'dark' : 'light');
      document.documentElement.setAttribute('data-theme', theme);
    } catch(e) {}
  })();
</script>

No Next.js, isso vai em um _document.tsx ou no layout.tsx raiz do App Router. Como esse script é executado sincronamente antes de qualquer CSS ser analisado, não há flash.


Temas com Tailwind CSS

Tailwind v3: Modo Escuro Baseado em Classe

No Tailwind v3, o modo escuro requer definir darkMode: 'class' em tailwind.config.js. Isso faz o Tailwind aplicar utilitários dark: somente quando a classe .dark está no elemento <html>:

// tailwind.config.js
module.exports = {
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        brand: {
          50: '#EFF6FF',
          100: '#DBEAFE',
          500: '#3B82F6',
          600: '#2563EB',
          700: '#1D4ED8',
          900: '#1E3A8A',
          950: '#172554',
        },
      },
    },
  },
};

Componentes usam prefixos dark: para variantes de modo escuro:

<div className="bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100">
  <button className="bg-brand-600 dark:bg-brand-500 text-white hover:bg-brand-700 dark:hover:bg-brand-400">
    Ação Principal
  </button>
</div>

O toggle de tema define a classe .dark em document.documentElement — a mesma abordagem que o padrão de atributo de dados, apenas usando uma classe em vez disso.

Tailwind v4: Configuração CSS-First

O Tailwind v4 move a configuração inteiramente para CSS, usando propriedades customizadas CSS nativamente:

/* styles.css */
@import "tailwindcss";

@theme {
  --color-brand-50: #EFF6FF;
  --color-brand-100: #DBEAFE;
  --color-brand-500: #3B82F6;
  --color-brand-600: #2563EB;
  --color-brand-700: #1D4ED8;
}

O Tailwind v4 gera classes utilitárias a partir dessas propriedades customizadas, e elas são imediatamente utilizáveis em JSX: className="bg-brand-600 text-white". A configuração do modo escuro no v4 usa @variant dark:

@variant dark (&:where([data-theme="dark"] *)) {
  /* Tailwind aplica utilitários dark: baseado nesse seletor */
}

Essa variante diz ao Tailwind para aplicar utilitários dark: quando um ancestral tem data-theme="dark", integrando nativamente com o padrão de atributo de dados.


Estratégia de Tematização Multi-Marca

O Desafio

Uma plataforma SaaS atendendo a múltiplos clientes, um produto white-label ou um sistema de design compartilhado entre múltiplas marcas de produtos precisa lidar não apenas com variantes claro/escuro, mas com identidades de cores completamente diferentes — cada uma com seu próprio primário de marca, destaque, cores de status e neutros.

Tokens de Marca como Substituições de Variáveis CSS

A abordagem mais fácil de manter define tokens semânticos agnósticos à marca em uma folha de estilos base e depois substitui as atribuições primitivas por marca:

/* Tema base — mesmos nomes de token para todas as marcas */
:root {
  /* Primitivos de Marca — substituídos por marca */
  --brand-primary-raw: 37 99 235; /* #2563EB em canais RGB */
  --brand-accent-raw: 99 102 241; /* #6366F1 em canais RGB */

  /* Tokens semânticos — computados a partir de primitivos */
  --brand-primary: rgb(var(--brand-primary-raw));
  --brand-primary-hover: color-mix(in srgb, rgb(var(--brand-primary-raw)) 80%, black);
  --brand-primary-subtle: color-mix(in srgb, rgb(var(--brand-primary-raw)) 10%, white);
}

/* Marca: Acme Corp (azul) */
[data-brand="acme"] {
  --brand-primary-raw: 37 99 235;   /* #2563EB */
  --brand-accent-raw: 16 185 129;   /* #10B981 */
}

/* Marca: Globex Inc (roxo) */
[data-brand="globex"] {
  --brand-primary-raw: 124 58 237;  /* #7C3AED */
  --brand-accent-raw: 245 158 11;   /* #F59E0B */
}

/* Marca: Initech (verde) */
[data-brand="initech"] {
  --brand-primary-raw: 22 163 74;   /* #16A34A */
  --brand-accent-raw: 239 68 68;    /* #EF4444 */
}

Usar a técnica de canais RGB brutos (37 99 235) permite variantes de opacidade sem tokens adicionais:

.overlay {
  background-color: rgba(var(--brand-primary-raw), 0.1);
}

Context React Multi-Marca

Estenda o contexto de tema para lidar com a marca:

// contexts/ThemeContext.tsx
type Brand = 'acme' | 'globex' | 'initech' | 'default';
type Theme = 'light' | 'dark';

interface ThemeContextValue {
  theme: Theme;
  brand: Brand;
  setTheme: (theme: Theme) => void;
  setBrand: (brand: Brand) => void;
}

export function ThemeProvider({ initialBrand = 'default', children }: {
  initialBrand?: Brand;
  children: React.ReactNode;
}) {
  const [theme, setTheme] = useState<Theme>('light');
  const [brand, setBrand] = useState<Brand>(initialBrand);

  useEffect(() => {
    const root = document.documentElement;
    root.setAttribute('data-theme', theme);
    root.setAttribute('data-brand', brand);
  }, [theme, brand]);

  return (
    <ThemeContext.Provider value={{ theme, brand, setTheme, setBrand }}>
      {children}
    </ThemeContext.Provider>
  );
}

Defina a marca inicial a partir do seu roteamento ou camada de autenticação — o subdomínio ou ID de tenant de cada cliente mapeia para um identificador de marca.

Gerando Escalas de Marca com o Gerador de Tonalidades

Para cada marca, você precisa de uma escala de cores completa, não apenas um ou dois valores hex. Use o Gerador de Tonalidades para gerar uma escala de 50–950 para a cor principal e de destaque de cada marca. Insira o hex principal da marca e obtenha a faixa completa de variantes escuro-para-claro.

Para a Globex Inc com o primário #7C3AED, o gerador de tonalidades produz a escala roxa completa. Para a Initech com #16A34A, a escala verde completa. Essas escalas então preenchem os tokens primitivos para cada marca, garantindo que --brand-primary-subtle (o tinte mais claro) e --brand-primary-hover (um estado pressionado mais escuro) permaneçam harmonicamente consistentes dentro de cada identidade de marca.


Principais Conclusões

  • Propriedades customizadas CSS são a base: Defina tokens semânticos (--text-primary, --brand-primary) e mude seus valores por tema, em vez de aplicar condicionalmente classes em cada componente.
  • A troca de tema por atributo de dados (data-theme="dark") é o padrão mais flexível — é substituível por JavaScript, compatível com SSR e combinável com media queries prefers-color-scheme.
  • O sistema de tokens de duas camadas (escala de cores primitiva + tokens de papel semântico) permite flexibilidade total de tema enquanto mantém os componentes limpos: primitivos definem sua paleta, semânticos definem como as cores são usadas.
  • Use o Gerador de Tonalidades para gerar uma escala completa de cores de 50–950 a partir do seu primário de marca — isso fornece toda a gama de variantes claras e escuras necessárias para um sistema de tokens completo.
  • O React Context gerencia o estado do tema (o nome do tema ativo) e efeitos colaterais (definir o atributo de dados, persistir no localStorage); componentes nunca precisam ler o contexto para aplicar as cores corretas.
  • Previna flash do tema errado com um script inline de bloqueio no <head> que lê localStorage e aplica o atributo de tema antes do navegador renderizar qualquer pixel.
  • Integração com Tailwind CSS: v3 usa darkMode: 'class' com prefixos dark:; v4 usa configuração CSS-first @theme com suporte nativo a variáveis CSS.
  • Tematização multi-marca empilha outro atributo de dados (data-brand) sobre o sistema de temas, substituindo valores de token primitivos por marca enquanto mantém tokens semânticos e componentes inalterados.

Cores relacionadas

Marcas relacionadas

Ferramentas relacionadas