Voltar para Artigos
Front-end10 min de leitura

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.

12 de agosto de 2026

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.ts
// 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.

themes.ts
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.

styled.d.ts (tipagem do theme nas props)
// 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.

ThemeProvider.tsx
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.

styles.ts
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.