Design System com styled-components: Tokens, Temas e Dark Mode
Tokens de design como fonte única de verdade, ThemeProvider tipado com module augmentation, dark mode com prefers-color-scheme e useState, componentes polimórficos com 'as' prop e por que CSS Variables costumam ser uma alternativa mais simples.
Hardcodar color: '#ff9000' em 30 componentes cria uma dívida técnica cara: uma mudança de identidade visual exige busca e substituição em dezenas de arquivos com risco de inconsistência. O Design System com tokens resolve isso: todas as cores, espaçamentos e tipografias vivem em uma única fonte de verdade. Mudar o primário de laranja para azul é alterar uma linha.
A hierarquia de um design system bem estruturado segue três camadas: tokens (os valores brutos — hex de cores, rem de espaçamentos), temas (mapeamento semântico dos tokens — 'primário', 'fundo', 'borda') e componentes (consomem o tema via props). Tokens nunca são usados diretamente nos componentes. Isso garante que o dark mode funcione automaticamente: basta trocar o objeto de tema e os componentes se adaptam sem mudança de código.
Tokens de Design
Os tokens são declarados com as const para que o TypeScript infira os valores literais (como '#f97316' ao invés de apenas string). Isso garante autocomplete completo ao referenciar os tokens para montar os temas. Os tokens são organizados por categoria (cores, tipografia, espaçamento, raios de borda) e por escala numérica — uma convenção herdada do Tailwind que facilita a comunicação com designers.
// Tokens: os valores primitivos do design system
// Usados para compor os temas — nunca usados diretamente nos componentes
export const tokens = {
colors: {
brand: {
50: '#fff7ed',
100: '#ffedd5',
500: '#f97316',
600: '#ea580c',
700: '#c2410c',
900: '#7c2d12',
},
gray: {
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
400: '#9ca3af',
600: '#4b5563',
800: '#1f2937',
900: '#111827',
},
feedback: {
error: '#dc2626',
success: '#16a34a',
warning: '#d97706',
info: '#2563eb',
},
},
fontSizes: {
xs: '0.75rem',
sm: '0.875rem',
md: '1rem',
lg: '1.125rem',
xl: '1.25rem',
'2xl': '1.5rem',
'4xl': '2.25rem',
},
spacing: {
1: '0.25rem',
2: '0.5rem',
4: '1rem',
6: '1.5rem',
8: '2rem',
12: '3rem',
16: '4rem',
},
radii: {
sm: '0.25rem',
md: '0.375rem',
lg: '0.5rem',
full: '9999px',
},
} as const;Temas Light e Dark
Os temas mapeiam os tokens para nomes semânticos que os componentes usam. A chave é que o darkTheme tem o mesmo formato que o lightTheme — o TypeScript garante isso com typeof lightTheme. Quando o tema muda, os componentes que usam p.theme.colors.primary automaticamente passam a receber a cor correspondente do novo tema, sem que o componente precise saber qual tema está ativo.
import { tokens } from './tokens';
export const lightTheme = {
colors: {
primary: tokens.colors.brand[600],
primaryHover: tokens.colors.brand[700],
background: tokens.colors.gray[50],
surface: '#ffffff',
border: tokens.colors.gray[200],
text: {
primary: tokens.colors.gray[900],
secondary: tokens.colors.gray[600],
disabled: tokens.colors.gray[400],
},
feedback: tokens.colors.feedback,
},
fontSizes: tokens.fontSizes,
spacing: tokens.spacing,
radii: tokens.radii,
};
export const darkTheme: typeof lightTheme = {
...lightTheme,
colors: {
primary: tokens.colors.brand[500],
primaryHover: tokens.colors.brand[600],
background: tokens.colors.gray[900],
surface: tokens.colors.gray[800],
border: tokens.colors.gray[600],
text: {
primary: tokens.colors.gray[50],
secondary: tokens.colors.gray[400],
disabled: tokens.colors.gray[600],
},
feedback: tokens.colors.feedback,
},
};
export type AppTheme = typeof lightTheme;ThemeProvider Tipado (module augmentation)
Por padrão, o p.theme dentro de um styled-component é do tipo DefaultTheme do styled-components, que é basicamente {} — sem autocomplete. O module augmentation sobrescreve essa interface vazia com o tipo do nosso tema real. Após adicionar o arquivo styled.d.ts, o p.theme.colors.primary passa a ter autocomplete completo e erros de compilação se você referenciar uma propriedade que não existe no tema.
// Estende o DefaultTheme do styled-components com o tipo do nosso tema
// Após isso, ${(p) => p.theme.colors.primary} tem autocomplete completo
import 'styled-components';
import type { AppTheme } from './themes';
declare module 'styled-components' {
export interface DefaultTheme extends AppTheme {}
}O ThemeProvider combina três responsabilidades: detectar a preferência do sistema operacional (prefers-color-scheme), persistir a escolha do usuário no localStorage, e injetar o tema correto em todos os componentes filhos via Context API. O GlobalStyle usa as variáveis do tema para aplicar os estilos base — fundo e cor do texto — com transição suave ao trocar de tema.
import { useState, useEffect } from 'react';
import { ThemeProvider as StyledThemeProvider, createGlobalStyle } from 'styled-components';
import { lightTheme, darkTheme } from '../styles/themes';
const GlobalStyle = createGlobalStyle`
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background-color: ${(p) => p.theme.colors.background};
color: ${(p) => p.theme.colors.text.primary};
font-family: 'Inter', sans-serif;
transition: background-color 0.2s, color 0.2s;
}
`;
export function ThemeProvider({ children }: { children: React.ReactNode }) {
// Respeita a preferência do sistema operacional como padrão
const systemPreference = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
const [mode, setMode] = useState<'light' | 'dark'>(
() => (localStorage.getItem('theme') as 'light' | 'dark') ?? systemPreference
);
useEffect(() => {
localStorage.setItem('theme', mode);
}, [mode]);
const theme = mode === 'dark' ? darkTheme : lightTheme;
return (
<StyledThemeProvider theme={theme}>
<GlobalStyle />
{children}
</StyledThemeProvider>
);
}
// Hook de acesso ao tema e toggle
export function useThemeToggle() {
// Use Context API para expor o setMode
}Componentes com Variantes Tipadas
O padrão de variantes em objetos é superior aos ternários aninhados dentro do template literal do styled-component. Em vez de ${(p) => p.variant === 'primary' ? '...' : p.variant === 'secondary' ? '...' : '...'}, você declara um objeto que mapeia cada variante para um bloco css. O código fica com estrutura de dado ao invés de uma cadeia de condicionais — muito mais legível e fácil de adicionar novas variantes.
import styled, { css } from 'styled-components';
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
type ButtonSize = 'sm' | 'md' | 'lg';
interface ButtonProps {
variant?: ButtonVariant;
size?: ButtonSize;
fullWidth?: boolean;
}
const variants = {
primary: css`
background: ${(p) => p.theme.colors.primary};
color: #fff;
&:hover { background: ${(p) => p.theme.colors.primaryHover}; }
`,
secondary: css`
background: ${(p) => p.theme.colors.surface};
color: ${(p) => p.theme.colors.text.primary};
border: 1px solid ${(p) => p.theme.colors.border};
`,
ghost: css`
background: transparent;
color: ${(p) => p.theme.colors.primary};
`,
danger: css`
background: ${(p) => p.theme.colors.feedback.error};
color: #fff;
`,
};
const sizes = {
sm: css`padding: ${(p) => p.theme.spacing[2]} ${(p) => p.theme.spacing[4]}; font-size: ${(p) => p.theme.fontSizes.sm};`,
md: css`padding: ${(p) => p.theme.spacing[4]} ${(p) => p.theme.spacing[6]}; font-size: ${(p) => p.theme.fontSizes.md};`,
lg: css`padding: ${(p) => p.theme.spacing[4]} ${(p) => p.theme.spacing[8]}; font-size: ${(p) => p.theme.fontSizes.lg};`,
};
export const StyledButton = styled.button<ButtonProps>`
border: none;
border-radius: ${(p) => p.theme.radii.md};
cursor: pointer;
font-weight: 600;
transition: all 0.15s;
width: ${(p) => (p.fullWidth ? '100%' : 'auto')};
${(p) => variants[p.variant ?? 'primary']}
${(p) => sizes[p.size ?? 'md']}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`;styled-components vs CSS Variables: Para projetos novos em 2024+, considere usar CSS Custom Properties (variáveis CSS nativas) com data-theme no elemento raiz — sem dependência, com melhor performance (sem runtime JS para interpolação) e compatível com qualquer framework. styled-components ainda é excelente para componentes com lógica de estilo complexa baseada em props, mas CSS Variables são mais simples para theming.
Conclusão
O Design System com tokens → temas → componentes cria uma hierarquia onde decisões de design existem em um único lugar. A tipagem via module augmentation do styled-components torna o p.theme.colors.primary com autocomplete e proteção de tipo. Variantes tipadas em objetos css eliminam os ternários aninhados nos estilos. E o ThemeProvider com prefers-color-scheme garante que o dark mode funcione automaticamente sem o usuário configurar nada.