Tutoriais

Construindo um Sistema de Cores para Seu Design System

12 min de leitura

Escolhas de cores ad hoc se acumulam em caos. Um azul de marca especificado como #3B82F6 em um componente, #2563EB em outro e rgba(59, 130, 246, 0.8) em um terceiro — todos ostensivamente o "mesmo" azul — cria uma inconsistência visual que prejudica a coesão da marca, torna o modo escuro impossível de implementar de forma confiável e transforma cada mudança de cor em um projeto de arqueologia na base de código.

Um sistema de cores resolve isso estabelecendo uma única fonte de verdade para cada cor da sua interface, organizada em duas camadas: uma paleta primitiva de todos os valores de cor disponíveis e uma camada de tokens semânticos que mapeia esses primitivos para seus papéis pretendidos. Essa separação torna possível construir modo escuro, modo de alto contraste e rebranding de marca mudando as atribuições de tokens semânticos — sem tocar em nenhum código de componente.

Este guia percorre a construção de um sistema de cores completo e pronto para produção do zero: escolher uma paleta primitiva, gerar escalas de tonalidades, definir tokens semânticos, implementá-los em CSS e Tailwind, e configurar documentação e testes.


Por Que Cores Sistemáticas São Importantes

O argumento para cores sistemáticas não é estético — é operacional. Veja o que quebra sem um sistema:

Inconsistência se acumula ao longo do tempo: No início de um projeto, designers e desenvolvedores tomam decisões de cor ad hoc que "parecem certas". Após dois anos, uma base de código acumula 40 valores distintos de azul que deveriam ser todos o mesmo azul. Ninguém sabe qual é o correto.

O modo escuro requer mapeamento semântico: O modo escuro não pode funcionar se as cores estiverem codificadas pelo seu valor visual. color: #1E293B é uma cor escura que fica boa como texto em um fundo claro. No modo escuro, você precisa de um valor diferente. Se você codificou #1E293B em 80 componentes, você tem 80 lugares para atualizar — ou um sistema complexo de substituição CSS que cria uma nova camada de dívida técnica. Tokens semânticos eliminam isso.

Mudanças de tema são caras: Rebranding ou mudar uma cor primária deveria ser uma única mudança. Com um sistema semântico, você muda --color-primary-600 no arquivo de token e cada botão, link, anel de foco e borda que o referencia atualiza automaticamente.

Auditorias de acessibilidade falham silenciosamente: Sem um sistema, verificar se todo o texto passa nos requisitos de contraste WCAG exige auditar manualmente cada combinação de texto/fundo. Com tokens semânticos, você pode definir regras ("text-primary em surface-primary deve passar AA") e testá-las programaticamente.


Convenções de Nomenclatura: Primitivo vs. Semântico

Um sistema de cores bem estruturado tem dois níveis de nomenclatura que servem a propósitos diferentes.

Tokens de Cor Primitivos

Tokens primitivos são os valores de cor brutos, nomeados por sua posição em uma escala. Eles não fazem suposições sobre o uso — são simplesmente uma paleta de cores disponíveis:

blue-50   → #EFF6FF
blue-100  → #DBEAFE
blue-200  → #BFDBFE
blue-300  → #93C5FD
blue-400  → #60A5FA
blue-500  → #3B82F6
blue-600  → #2563EB
blue-700  → #1D4ED8
blue-800  → #1E40AF
blue-900  → #1E3A8A
blue-950  → #172554

Os nomes de tokens primitivos respondem: "Que cor é essa?" Não respondem: "Para que serve essa cor?"

Tokens de Cor Semânticos

Tokens semânticos referenciam tokens primitivos e nomeiam cores pelo seu propósito:

color-action-primary       → blue-600
color-action-primary-hover → blue-700
color-action-primary-text  → white-0

color-surface-default      → neutral-50 (modo claro) / neutral-900 (modo escuro)
color-surface-elevated     → white-0 (modo claro) / neutral-800 (modo escuro)

color-text-primary         → neutral-900 (modo claro) / neutral-50 (modo escuro)
color-text-secondary       → neutral-600 (modo claro) / neutral-400 (modo escuro)
color-text-disabled        → neutral-400 (modo claro) / neutral-600 (modo escuro)

color-border-default       → neutral-200 (modo claro) / neutral-700 (modo escuro)
color-border-focused       → blue-500

Os nomes de tokens semânticos respondem: "Para que serve essa cor?" Eles não especificam um valor — referenciam um primitivo.

Essa separação é a decisão estrutural fundamental. Componentes referenciam tokens semânticos, não primitivos. Quando você define o modo escuro, remapeia tokens semânticos para primitivos diferentes — nenhum código de componente muda.

Esquemas de Nomenclatura em Uso

Diferentes sistemas de design usam diferentes convenções de nomenclatura para sua camada semântica:

Sistema Exemplo de Nome Semântico Convenção
Material Design sys.color.primary Hierarquia de papéis separada por pontos
Apple HIG label, secondaryLabel Nomes de papel simples
Radix UI --accent-9, --gray-11 Nomes semânticos baseados em escala
Primer (GitHub) fgColor.default, bgColor.default Papel + variante

A convenção importa menos do que a consistência. Escolha um esquema e aplique-o em todo o projeto.


Geração de Escala de Tonalidades (50–950)

Uma escala de tonalidades é uma série ordenada de valores de cor de quase-branco a quase-preto dentro de uma única família de matiz. O Tailwind CSS popularizou a escala 50–950; o Material Design usa 100–900. Os números específicos importam menos do que ter passos suficientes para cobrir a gama de casos de uso: tintes muito claros para fundos, valores intermediários para elementos interativos e sombras escuras para texto.

O Que Torna uma Boa Escala de Tonalidades

Uma escala de tonalidades utilizável tem duas propriedades:

  1. Passos perceptuais suficientes: Tonalidades adjacentes devem ser visualmente distintas — não tão próximas que a diferença seja imperceptível, nem tão distantes que apareçam lacunas na cobertura.
  2. Progressão de luminosidade consistente: Os valores de luminosidade devem aumentar previsivelmente de 950 (mais escuro) a 50 (mais claro), sem saltos ou inversões surpreendentes.

O modo de falha para escalas de tonalidades selecionadas manualmente é o espaçamento irregular de luminosidade. Uma escala onde as tonalidades 300, 400 e 500 são perceptualmente similares e depois de 500 para 700 salta dramaticamente não é útil — você sempre vai pegar o 500 quando precisa de algo no meio da faixa.

Gerando Tonalidades em OKLCH

OKLCH (OK Lightness-Chroma-Hue) é o melhor espaço de cor para geração de tonalidades porque seu canal de luminosidade é perceptualmente uniforme — passos numéricos iguais produzem passos perceptuais iguais. Se você espaçar os valores de luminosidade de L=0,15 (muito escuro) a L=0,98 (muito claro) em incrementos iguais, as tonalidades resultantes parecerão igualmente espaçadas visualmente.

Para gerar uma escala de tonalidades de 11 passos em OKLCH:

  1. Escolha seu matiz e croma base: por exemplo, H=250 (azul), C=0,20.
  2. Espaçe os valores de luminosidade uniformemente: L = 0,15, 0,22, 0,30, 0,38, 0,47, 0,55, 0,63, 0,72, 0,82, 0,92, 0,97.
  3. Opcionalmente, varie ligeiramente o croma: reduza o croma nos extremos (muito claro e muito escuro) onde cores saturadas parecem fora de lugar.
  4. Converta os valores OKLCH para hex para implementação.

Uma escala OKLCH-para-hex prática para um matiz azul:

Tonalidade | OKLCH                    | Hex
-----------+--------------------------+--------
50    | oklch(0.97 0.04 250)     | #EFF6FF
100   | oklch(0.92 0.07 250)     | #DBEAFE
200   | oklch(0.84 0.11 250)     | #BFDBFE
300   | oklch(0.74 0.16 250)     | #93C5FD
400   | oklch(0.65 0.19 250)     | #60A5FA
500   | oklch(0.55 0.22 250)     | #3B82F6
600   | oklch(0.47 0.22 250)     | #2563EB
700   | oklch(0.40 0.20 250)     | #1D4ED8
800   | oklch(0.32 0.18 250)     | #1E40AF
900   | oklch(0.24 0.14 250)     | #1E3A8A
950   | oklch(0.17 0.10 250)     | #172554

Use o Gerador de Tonalidades do ColorFYI para gerar escalas de tonalidades no estilo Tailwind a partir de qualquer cor base, e o Conversor de Cores para inspecionar cada tonalidade gerada no espaço OKLCH para verificar a progressão de luminosidade.

Quantos Passos de Tonalidade?

Para a maioria dos sistemas de design, 9–11 passos é a faixa certa:

  • 50 e 950: Valores de borda para fundos de superfície e uso mais profundo de texto/borda.
  • 100–200: Fundos levemente tingidos (fundos de alertas, estados de hover em superfícies brancas).
  • 300–400: Cores de ícone, texto de placeholder, estados desabilitados.
  • 500: Frequentemente a cor de marca "pura" — a que os padrões de cor da marca especificam.
  • 600–700: Preenchimentos de elementos interativos (botões, links) onde o contraste contra o branco é necessário.
  • 800–900: Uso de texto e texto-sobre-fundo-colorido.

Gere tonalidades para cada família de matiz na sua paleta: a cor principal da sua marca, uma série de cinza neutro e quaisquer cores de destaque (vermelho/erro, verde/sucesso, amarelo/aviso, etc.).


Tokens de Cor Semânticos

Com os primitivos estabelecidos, defina tokens semânticos que mapeiam primitivos para papéis. A granularidade desse mapeamento determina a flexibilidade do sistema.

Cores de Superfície

Tokens de superfície definem as camadas de fundo da sua UI:

:root {
  --color-surface-base: var(--neutral-50);       /* Fundo da página */
  --color-surface-elevated: var(--white);         /* Fundos de card e folha */
  --color-surface-overlay: var(--white);          /* Fundos de modal e popover */
  --color-surface-sunken: var(--neutral-100);     /* Campos internos, blocos de código */
}

[data-theme="dark"] {
  --color-surface-base: var(--neutral-950);
  --color-surface-elevated: var(--neutral-900);
  --color-surface-overlay: var(--neutral-800);
  --color-surface-sunken: var(--neutral-950);
}

Cores de Texto

Tokens de texto cobrem cada papel de texto na hierarquia:

:root {
  --color-text-primary: var(--neutral-900);      /* Texto do corpo, títulos */
  --color-text-secondary: var(--neutral-600);    /* Legendas, rótulos */
  --color-text-tertiary: var(--neutral-400);     /* Placeholder, dica */
  --color-text-disabled: var(--neutral-300);     /* Estado desabilitado */
  --color-text-inverse: var(--white);            /* Texto em fundos escuros/coloridos */
  --color-text-link: var(--blue-600);
  --color-text-link-hover: var(--blue-700);
}

[data-theme="dark"] {
  --color-text-primary: var(--neutral-50);
  --color-text-secondary: var(--neutral-400);
  --color-text-tertiary: var(--neutral-600);
  --color-text-disabled: var(--neutral-700);
  --color-text-link: var(--blue-400);
  --color-text-link-hover: var(--blue-300);
}

Cores Interativas / de Ação

Tokens de ação cobrem cada estado dos elementos interativos:

:root {
  /* Ação primária */
  --color-action-primary-bg: var(--blue-600);
  --color-action-primary-bg-hover: var(--blue-700);
  --color-action-primary-bg-active: var(--blue-800);
  --color-action-primary-text: var(--white);

  /* Ação secundária / ghost */
  --color-action-secondary-border: var(--blue-600);
  --color-action-secondary-text: var(--blue-600);
  --color-action-secondary-bg-hover: var(--blue-50);

  /* Ação destrutiva */
  --color-action-danger-bg: var(--red-600);
  --color-action-danger-bg-hover: var(--red-700);
  --color-action-danger-text: var(--white);
}

Cores de Feedback / Status

Tokens de status mapeiam cores funcionais para seu significado semântico:

:root {
  /* Sucesso */
  --color-status-success-bg: var(--green-50);
  --color-status-success-border: var(--green-200);
  --color-status-success-text: var(--green-700);
  --color-status-success-icon: var(--green-500);

  /* Aviso */
  --color-status-warning-bg: var(--yellow-50);
  --color-status-warning-border: var(--yellow-200);
  --color-status-warning-text: var(--yellow-800);
  --color-status-warning-icon: var(--yellow-500);

  /* Erro */
  --color-status-error-bg: var(--red-50);
  --color-status-error-border: var(--red-200);
  --color-status-error-text: var(--red-700);
  --color-status-error-icon: var(--red-500);

  /* Informação */
  --color-status-info-bg: var(--blue-50);
  --color-status-info-border: var(--blue-200);
  --color-status-info-text: var(--blue-700);
  --color-status-info-icon: var(--blue-500);
}

Cores de Borda

:root {
  --color-border-default: var(--neutral-200);
  --color-border-strong: var(--neutral-400);
  --color-border-focused: var(--blue-500);
  --color-border-error: var(--red-500);
  --color-border-disabled: var(--neutral-200);
}

[data-theme="dark"] {
  --color-border-default: var(--neutral-700);
  --color-border-strong: var(--neutral-500);
  --color-border-focused: var(--blue-400);
  --color-border-error: var(--red-400);
  --color-border-disabled: var(--neutral-700);
}

Implementação em CSS

A implementação CSS usa propriedades customizadas definidas no nível :root (ou em seletores [data-theme] para tematização). Componentes referenciam tokens semânticos, nunca primitivos diretamente.

Estrutura de Arquivos

design-system/
├── tokens/
│   ├── primitives.css       # Todos os valores de cor primitivos
│   ├── semantic.css         # Mapeamento token semântico → primitivo
│   └── dark.css             # Substituições semânticas para modo escuro
└── components/
    ├── button.css
    ├── card.css
    └── ...

primitives.css

:root {
  /* Escala neutra */
  --neutral-50: #F8FAFC;
  --neutral-100: #F1F5F9;
  --neutral-200: #E2E8F0;
  --neutral-300: #CBD5E1;
  --neutral-400: #94A3B8;
  --neutral-500: #64748B;
  --neutral-600: #475569;
  --neutral-700: #334155;
  --neutral-800: #1E293B;
  --neutral-900: #0F172A;
  --neutral-950: #020617;

  /* Escala azul */
  --blue-50: #EFF6FF;
  --blue-100: #DBEAFE;
  --blue-200: #BFDBFE;
  --blue-300: #93C5FD;
  --blue-400: #60A5FA;
  --blue-500: #3B82F6;
  --blue-600: #2563EB;
  --blue-700: #1D4ED8;
  --blue-800: #1E40AF;
  --blue-900: #1E3A8A;
  --blue-950: #172554;

  /* Branco e preto */
  --white: #FFFFFF;
  --black: #000000;
}

Uso em Componentes

Componentes referenciam apenas tokens semânticos:

.button-primary {
  background-color: var(--color-action-primary-bg);
  color: var(--color-action-primary-text);
  border: none;
}

.button-primary:hover {
  background-color: var(--color-action-primary-bg-hover);
}

.button-primary:active {
  background-color: var(--color-action-primary-bg-active);
}

.button-primary:disabled {
  background-color: var(--neutral-200);  /* Exceção: estado desabilitado frequentemente usa primitivos */
  color: var(--color-text-disabled);
}

Quando o tema muda de claro para escuro, --color-action-primary-bg resolve automaticamente para um valor diferente — sem substituições de modo escuro específicas para o botão.


Implementação no Tailwind CSS

O Tailwind v3 e v4 suportam integração de tokens de design pelo arquivo de configuração e pela abordagem de variáveis CSS.

Config do Tailwind v3

Defina sua escala de cores em tailwind.config.js e mapeie para propriedades customizadas CSS:

// tailwind.config.js
const colors = require("tailwindcss/colors");

module.exports = {
  theme: {
    colors: {
      transparent: "transparent",
      current: "currentColor",
      white: "#ffffff",
      black: "#000000",

      // Escalas primitivas
      neutral: {
        50:  "#F8FAFC",
        100: "#F1F5F9",
        200: "#E2E8F0",
        300: "#CBD5E1",
        400: "#94A3B8",
        500: "#64748B",
        600: "#475569",
        700: "#334155",
        800: "#1E293B",
        900: "#0F172A",
        950: "#020617",
      },
      blue: {
        50:  "#EFF6FF",
        100: "#DBEAFE",
        200: "#BFDBFE",
        300: "#93C5FD",
        400: "#60A5FA",
        500: "#3B82F6",
        600: "#2563EB",
        700: "#1D4ED8",
        800: "#1E40AF",
        900: "#1E3A8A",
        950: "#172554",
      },

      // Tokens semânticos via variáveis CSS
      "action-primary": "var(--color-action-primary-bg)",
      "action-primary-hover": "var(--color-action-primary-bg-hover)",
      "surface": "var(--color-surface-base)",
      "surface-elevated": "var(--color-surface-elevated)",
      "text-primary": "var(--color-text-primary)",
      "text-secondary": "var(--color-text-secondary)",
      "border-default": "var(--color-border-default)",
    },
  },
};

Com essa configuração, você pode escrever bg-action-primary e ele resolve para o valor de token semântico correto, que por sua vez resolve para o primitivo correto para o tema atual.

Config CSS-First do Tailwind v4

O Tailwind v4 substitui a configuração JavaScript por uma abordagem CSS-first usando @theme:

@import "tailwindcss";

@theme {
  /* Cores primitivas */
  --color-blue-500: #3B82F6;
  --color-blue-600: #2563EB;
  --color-blue-700: #1D4ED8;
  --color-neutral-50: #F8FAFC;
  --color-neutral-900: #0F172A;

  /* Tokens semânticos */
  --color-surface: var(--color-surface-base);
  --color-text: var(--color-text-primary);
  --color-primary: var(--color-action-primary-bg);
}

/* Definições semânticas */
:root {
  --color-surface-base: var(--color-neutral-50);
  --color-text-primary: var(--color-neutral-900);
  --color-action-primary-bg: var(--color-blue-600);
}

[data-theme="dark"] {
  --color-surface-base: oklch(0.145 0 0);
  --color-text-primary: var(--color-neutral-50);
  --color-action-primary-bg: var(--color-blue-500);
}

Testes e Documentação

Um sistema de cores só é útil se for compreendido e implementado de forma confiável por todos na equipe. Os testes capturam regressões; a documentação torna o sistema apreensível.

Testes de Contraste

Cada token de texto emparelhado com seu token de superfície de fundo pretendido deve ser verificado programaticamente para atender aos requisitos WCAG 2.1 AA (4,5:1 para texto normal, 3:1 para texto grande). Escreva testes que:

  1. Resolvam os valores atuais de propriedade customizada CSS (para os temas claro e escuro).
  2. Calculem a proporção de contraste WCAG entre cada par de texto/superfície.
  3. Afirmem que a proporção atende ao limite exigido.
import { getContrastRatio } from "some-color-utility";

const tokens = {
  light: {
    textPrimary: "#0F172A",
    surfaceBase: "#F8FAFC",
  },
  dark: {
    textPrimary: "#F8FAFC",
    surfaceBase: "#020617",
  }
};

describe("Contraste do sistema de cores", () => {
  test("text-primary em surface-base passa AA no modo claro", () => {
    const ratio = getContrastRatio(tokens.light.textPrimary, tokens.light.surfaceBase);
    expect(ratio).toBeGreaterThanOrEqual(4.5);
  });

  test("text-primary em surface-base passa AA no modo escuro", () => {
    const ratio = getContrastRatio(tokens.dark.textPrimary, tokens.dark.surfaceBase);
    expect(ratio).toBeGreaterThanOrEqual(4.5);
  });
});

Use o Verificador de Contraste do ColorFYI para verificação manual de pares de tokens individuais durante a definição de tokens, antes de escrever testes automatizados.

Documentação de Tokens de Cor

Documente cada token semântico com: - Nome: O nome exato da variável CSS. - Valor (claro): O primitivo que referencia no modo claro. - Valor (escuro): O primitivo no modo escuro. - Uso: Onde este token deve ser usado. - Antipadrões: Onde ele não deve ser usado.

| Token                        | Claro          | Escuro         | Uso                            |
|------------------------------|----------------|----------------|--------------------------------|
| --color-text-primary         | neutral-900    | neutral-50     | Texto do corpo, títulos        |
| --color-text-secondary       | neutral-600    | neutral-400    | Legendas, metadados, rótulos   |
| --color-action-primary-bg    | blue-600       | blue-500       | Fundo do botão primário        |
| --color-surface-elevated     | white          | neutral-900    | Fundos de card, folha, painel  |

Testes de Regressão Visual

Combine a documentação de tokens com testes de regressão visual usando ferramentas como Percy, Chromatic ou a comparação de capturas de tela do Playwright. Um teste de regressão visual renderiza sua biblioteca de componentes a cada mudança de token, capturando capturas de tela e comparando com uma linha de base — qualquer mudança de cor aciona uma revisão, capturando regressões acidentais.

Página de Paleta de Cores no Storybook

No Storybook, crie uma história de paleta de cores dedicada que renderiza todos os tokens como amostras:

// ColorPalette.stories.jsx
const tokens = [
  { name: "--color-surface-base", label: "Superfície / Base" },
  { name: "--color-surface-elevated", label: "Superfície / Elevada" },
  { name: "--color-text-primary", label: "Texto / Primário" },
  // ...
];

export function ColorPaletteDoc() {
  return (
    <div>
      {tokens.map(({ name, label }) => (
        <div key={name} style={{ display: "flex", alignItems: "center", gap: 16 }}>
          <div style={{
            width: 48,
            height: 48,
            backgroundColor: `var(${name})`,
            borderRadius: 4,
            border: "1px solid rgba(0,0,0,0.1)"
          }} />
          <div>
            <div>{label}</div>
            <code>{name}</code>
          </div>
        </div>
      ))}
    </div>
  );
}

Isso renderiza os valores reais do token em tempo real — em qualquer tema que esteja ativo no Storybook — tornando a documentação auto-verificável em vez de estática.


Principais Conclusões

  • Um sistema de cores separa tokens primitivos (nomeados por sua posição de valor em uma escala: blue-600) de tokens semânticos (nomeados por seu propósito: color-action-primary-bg). Componentes sempre referenciam tokens semânticos, nunca primitivos.
  • Essa separação permite o modo escuro e a tematização remapeando tokens semânticos para primitivos diferentes no nível de propriedade customizada CSS, sem nenhuma mudança de código de componente.
  • Gere escalas de tonalidades (50–950) usando o espaço de cor OKLCH onde o canal de luminosidade é perceptualmente uniforme — passos numéricos iguais produzem resultados visuais igualmente espaçados. Use o Gerador de Tonalidades do ColorFYI para criar escalas de tonalidades compatíveis com Tailwind a partir de qualquer cor âncora.
  • As categorias de tokens semânticos cobrem quatro áreas: superfície (camadas de fundo), texto (hierarquia de papéis de texto), ação (estados de elementos interativos) e feedback (sucesso, aviso, erro, informação).
  • Em CSS, defina primitivos em um arquivo e mapeamentos semânticos em outro. Use substituições do seletor [data-theme="dark"] para remapear tokens semânticos para o modo escuro.
  • No Tailwind v3, estenda o objeto de cores da configuração com referências de propriedade customizada CSS. No Tailwind v4, use o bloco @theme CSS-first para definir tanto tokens primitivos quanto semânticos.
  • Teste cada par de token de texto/superfície para conformidade de contraste WCAG — programaticamente, não apenas manualmente. Use o Verificador de Contraste do ColorFYI para verificação manual durante a definição de tokens.
  • Documente tokens no Storybook com amostras em tempo real que renderizam os valores reais de propriedade customizada CSS, garantindo que a documentação reflita o estado atual real do sistema.

Cores relacionadas

Marcas relacionadas

Ferramentas relacionadas