Artigo Build·Desenvolvimento·13 min de leitura de leitura

Paleta de Cores com CSS Variables (2026): Guia Completo de Design System Escalável

Paleta de cores bem feita é a espinha dorsal de qualquer design system. Este guia mostra como construir usando CSS custom properties, separar tokens primitivos de semânticos, gerar escala 50-950, implementar dark mode e aplicar em stacks modernas (Tailwind, shadcn, CSS puro).

Vitor Morais

Por Vitor Morais

Fundador do MochaLabz ·

🎨

Converta e compare cores

Cole HEX, RGB, HSL ou OKLCH e veja a cor em todos os formatos — essencial para design system.

Usar conversor →

Paleta de cores é a infraestrutura invisível de qualquer design system sério. Sem ela, você tem cores hardcoded em dezenas de componentes, inconsistência entre páginas e uma migração dolorosa toda vez que o brand muda um tom. Com ela, você altera um arquivo e o site inteiro ajusta — incluindo dark mode, temas alternativos e variações white-label.

Este guia cobre o método moderno: CSS custom properties, separação entre tokens primitivos e semânticos, geração de escala 50–950, suporte a dark mode e implementação em stacks populares. Tudo aplicável em produção hoje.

Por que CSS variables venceram SASS/LESS variables

Variáveis de pré-processador (SASS, LESS) são resolvidas no build — o CSS final tem a cor hardcoded. CSS custom properties são nativas, dinâmicas e poderosas:

  • Mudam em tempo real: troca de tema sem recarregar a página.
  • Herdam pela cascata: escopo natural por componente ou seção.
  • Respondem a media queries: dark mode automático via prefers-color-scheme.
  • Acessíveis via JavaScript: ler e alterar com document.documentElement.style.setProperty().
  • Não exigem build step: funcionam em CSS puro.

Dois níveis de tokens: primitivos e semânticos

Design systems maduros separam cores em duas camadas. Essa separação é a diferença entre uma paleta que cresce bem e uma que vira bagunça.

Tokens primitivos vs semânticos
CritérioDefiniçãoExemplo
PrimitivosPaleta crua, cores cruas--blue-500, --gray-100, --red-700
SemânticosUso contextual, apontam para primitivos--surface, --text-primary, --border

Contexto

A regra: componentes nunca usam tokens primitivos diretamente. Sempre via semântico. Um botão usa --primary, não --blue-500. Isso é o que permite dark mode (só muda os semânticos) e rebrand (só muda os primitivos) sem tocar nos componentes.

Estrutura base: tokens primitivos

Comece com os matizes essenciais (neutros + 1–2 accent) e 10 variações de lightness cada. Em OKLCH (recomendado em 2026):

:root { /* Neutros (escala de cinza) */ --gray-50: oklch(0.985 0.002 0); --gray-100: oklch(0.97 0.003 0); --gray-200: oklch(0.92 0.004 0); --gray-300: oklch(0.85 0.005 0); --gray-400: oklch(0.70 0.006 0); --gray-500: oklch(0.55 0.006 0); --gray-600: oklch(0.44 0.006 0); --gray-700: oklch(0.36 0.005 0); --gray-800: oklch(0.26 0.004 0); --gray-900: oklch(0.18 0.003 0); --gray-950: oklch(0.10 0.002 0); /* Azul (accent primário) */ --blue-50: oklch(0.97 0.02 259); --blue-100: oklch(0.93 0.05 259); --blue-200: oklch(0.87 0.09 259); --blue-300: oklch(0.79 0.14 259); --blue-400: oklch(0.72 0.17 259); --blue-500: oklch(0.62 0.19 259); --blue-600: oklch(0.54 0.19 259); --blue-700: oklch(0.46 0.18 259); --blue-800: oklch(0.36 0.14 259); --blue-900: oklch(0.28 0.10 259); --blue-950: oklch(0.20 0.07 259); /* Feedback */ --red-500: oklch(0.60 0.22 25); --green-500: oklch(0.60 0.18 145); --amber-500: oklch(0.75 0.18 80); }

Dica

OKLCH é perceptualmente uniforme: cada passo de 10% em L produz diferença visual consistente em qualquer matiz. Em HSL, amarelo 50% parece muito mais claro que azul 50%. Se você não quer aprender OKLCH agora, use HSL — ainda é melhor que HEX solto.

Tokens semânticos: nomeando por uso

Cada token semântico aponta para um primitivo e tem nome baseado em função, não em aparência.

:root { /* Superfícies */ --surface: var(--gray-50); --surface-2: var(--gray-100); --surface-3: var(--gray-200); /* Texto */ --text-primary: var(--gray-900); --text-secondary: var(--gray-600); --text-tertiary: var(--gray-500); --text-disabled: var(--gray-400); /* Bordas */ --border: var(--gray-200); --border-strong: var(--gray-400); /* Ação primária */ --primary: var(--blue-600); --primary-hover: var(--blue-700); --primary-active: var(--blue-800); /* Feedback */ --success: var(--green-500); --warning: var(--amber-500); --danger: var(--red-500); }

Atenção

Nome do token nunca deve indicar cor. --button-blue vira absurdo quando você muda pra verde. --primary continua fazendo sentido mesmo depois de 10 rebrands. Nomes semânticos envelhecem bem.

Usando os tokens em componentes

Componentes usam só tokens semânticos. Exemplo de botão primário:

.btn-primary { background: var(--primary); color: var(--surface); border: 1px solid var(--primary); padding: 0.75rem 1.5rem; border-radius: 0.5rem; } .btn-primary:hover { background: var(--primary-hover); border-color: var(--primary-hover); } .btn-primary:active { background: var(--primary-active); } .card { background: var(--surface); border: 1px solid var(--border); color: var(--text-primary); }

Dark mode: um único override

Com tokens semânticos, dark mode é só sobrescrever os semânticos — nenhum componente é tocado.

:root { /* ... tudo acima (modo claro) ... */ } [data-theme="dark"] { --surface: var(--gray-950); --surface-2: var(--gray-900); --surface-3: var(--gray-800); --text-primary: var(--gray-50); --text-secondary: var(--gray-300); --text-tertiary: var(--gray-400); --text-disabled: var(--gray-600); --border: var(--gray-700); --border-strong: var(--gray-500); /* Primary pode precisar de ajuste para contraste */ --primary: var(--blue-400); --primary-hover: var(--blue-300); --primary-active: var(--blue-500); }

Toggle de tema via JavaScript

function setTheme(theme) { document.documentElement.setAttribute("data-theme", theme); localStorage.setItem("theme", theme); } // Ao carregar a página const savedTheme = localStorage.getItem("theme") || "light"; setTheme(savedTheme); // Respeitar preferência do sistema se não houver preferência salva if (!localStorage.getItem("theme")) { const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; setTheme(prefersDark ? "dark" : "light"); }

Modo automático: respeitar sistema

Se você não quer toggle e prefere seguir o sistema do usuário:

@media (prefers-color-scheme: dark) { :root { --surface: var(--gray-950); --text-primary: var(--gray-50); /* ... */ } }

Escopo local: tema por seção

CSS variables herdam pela cascata. Você pode ter tema diferente por componente:

<section class="marketing-cta"> <button class="btn-primary">CTA</button> </section> <style> .marketing-cta { /* Neste bloco, primary vira laranja */ --primary: #F97316; --primary-hover: #EA580C; } </style>

Dica

Esse padrão é útil para landing pages A/B, seções patrocinadas ou componentes white-label. Um container redefine os tokens; tudo dentro herda. Zero duplicação de CSS.

Implementação em Tailwind CSS v4

Tailwind v4 (2024+) migrou pra CSS variables nativas. Configuração mínima:

/* app/globals.css */ @import "tailwindcss"; @theme { --color-primary-50: oklch(0.97 0.02 259); --color-primary-500: oklch(0.62 0.19 259); --color-primary-900: oklch(0.28 0.10 259); --color-surface: var(--color-gray-50); --color-text: var(--color-gray-900); } @media (prefers-color-scheme: dark) { @theme { --color-surface: var(--color-gray-900); --color-text: var(--color-gray-50); } }

Uso nas classes:

<div class="bg-surface text-text"> <button class="bg-primary-500 hover:bg-primary-600"> Primary CTA </button> </div>

Implementação em shadcn/ui

shadcn é o padrão de componentes em React em 2026. Usa CSS variables para tema completo. Configuração em globals.css:

@layer base { :root { --background: 0 0% 100%; --foreground: 240 10% 3.9%; --primary: 240 5.9% 10%; --primary-foreground: 0 0% 98%; --muted: 240 4.8% 95.9%; --muted-foreground: 240 3.8% 46.1%; --border: 240 5.9% 90%; /* ... */ } .dark { --background: 240 10% 3.9%; --foreground: 0 0% 98%; --primary: 0 0% 98%; --primary-foreground: 240 5.9% 10%; /* ... */ } }

Contexto

shadcn usa HSL com valores separados por espaço (sem hsl()) para permitir composição com alpha via hsl(var(--primary) / 0.5). Padrão que se popularizou e é útil em paletas com transparências variáveis.

Variáveis para espaçamento e outras dimensões

CSS variables não se limitam a cores. Use pra spacing, bordas, sombras, radius:

:root { /* Escala de espaçamento */ --space-1: 0.25rem; --space-2: 0.5rem; --space-4: 1rem; --space-8: 2rem; /* Border radius */ --radius-sm: 0.25rem; --radius: 0.5rem; --radius-lg: 1rem; --radius-full: 9999px; /* Sombras */ --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.05); --shadow: 0 1px 3px rgb(0 0 0 / 0.1); --shadow-lg: 0 10px 15px rgb(0 0 0 / 0.1); /* Transitions */ --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); --transition: 250ms cubic-bezier(0.4, 0, 0.2, 1); }

Acessibilidade: contraste WCAG em cada par

Toda combinação de texto + fundo precisa passar em WCAG AA (4.5:1 texto normal, 3:1 texto grande).

Pares típicos e contraste necessário
CritérioParContraste mínimo
--text-primary em --surfaceEx: gray-900 em gray-504.5:1
--text-secondary em --surfacegray-600 em gray-504.5:1
--primary-foreground em --primarywhite em blue-6004.5:1
--text-disabled em --surfacegray-400 em gray-503:1 (texto grande)
--border em --surfaceNão precisaN/A (elemento não-texto)

Atenção

Cada vez que você muda primitivos ou semânticos, revalide contraste. Ferramentas como WebAIM Contrast Checker e analisadores automáticos detectam falhas. Em dark mode, o ponto mais comum de falha é texto secundário em fundo muito escuro.

Gerando a escala a partir de uma cor base

Você não precisa manualmente escolher 10 cores. Use ferramentas:

  • uicolors.app: cole HEX da cor base, gera escala estilo Tailwind pronta para copiar. Oferece ajuste fino por step.
  • Tailwind Color Generator: similar, integrado ao ecossistema Tailwind.
  • Radix Colors: 12 steps por cor, pensado para UI (background, component, border, text, accent). Mais granular, menos customização.
  • Leonardo (Adobe): open source, gera paletas baseadas em requisitos de contraste.

Temas white-label: múltiplos brands no mesmo app

Quando você oferece o produto em white-label (marca do cliente), CSS variables são a solução. Cada cliente tem uma classe que sobrescreve primitivos:

[data-tenant="acme"] { --primary: oklch(0.55 0.20 15); /* vermelho */ --primary-hover: oklch(0.48 0.22 15); } [data-tenant="globex"] { --primary: oklch(0.60 0.18 145); /* verde */ --primary-hover: oklch(0.54 0.20 145); }

Erros clássicos

  • Nomes descritivos em vez de semânticos: --button-blue-medium vira problema quando você muda a cor.
  • Componentes usando tokens primitivos: quebra a abstração; rebrand vira retrabalho.
  • Sem camada semântica: paleta com só primitivos não permite dark mode elegante.
  • Sobrescrever globalmente: mudar --primary no :root dentro de componente afeta tudo, não só ele. Use escopo local.
  • Ignorar contraste em modo escuro: dark mode rascunhado quebra WCAG em texto secundário.
  • Deixar dezenas de cores sem token: cada nova cor ad hoc multiplica dívida técnica.

Ferramentas úteis

  • Conversor de cores (MochaLabz): HEX ↔ RGB ↔ HSL ↔ OKLCH com preview.
  • WebAIM Contrast Checker: valida WCAG AA/AAA.
  • Polypane / DevTools acessibilidade: simula color blindness.
  • APCA Contrast Calculator: algoritmo moderno de contraste que será padrão em WCAG 3.
  • Culori (biblioteca JS): manipulação de cores em OKLCH, conversão, interpolação.

Estrutura recomendada de arquivo

styles/ ├── tokens/ │ ├── primitives.css /* --blue-500, --gray-100, etc. */ │ ├── semantic.css /* --surface, --text-primary, etc. */ │ ├── spacing.css /* --space-1, --space-2 */ │ ├── radius.css /* --radius-sm, --radius */ │ └── shadow.css /* --shadow-sm, --shadow */ ├── themes/ │ ├── light.css /* valores semânticos do modo claro */ │ └── dark.css /* valores semânticos do modo escuro */ ├── components/ │ └── ... (usam só semânticos) └── globals.css /* importa tudo acima */

Migração: refatorando cores hardcoded

Se seu codebase está cheio de cores hardcoded, migração em três fases:

  1. Audite: grep de #[0-9a-fA-F]{3,6} e rgb(. Levante todas as cores únicas.
  2. Consolide em primitivos: 27 azuis diferentes viram 10 steps da escala blue.
  3. Crie semânticos: mapeie uso → semântico → primitivo.
  4. Substitua por componente: componente por componente, troque HEX por var().
  5. Adicione dark mode: agora que há camada semântica, dark vira override.

Vai mais fundo

Em codebase grande, ferramentas como Stylelint com regra declaration-property-value-disallowed-list podem proibir cores hardcoded no CSS novo. Força que toda cor venha de token. Regra simples que previne dívida técnica crescer.

Paleta em uma frase

Paleta de cores bem feita é uma infraestrutura que paga dividendos: dark mode em 10 linhas de CSS, rebrand em um arquivo, temas white-label sem duplicação. Tokens semânticos apontando para primitivos são o padrão de 2026 — Tailwind, shadcn, Radix, todos convergiram. Se sua paleta ainda é HEX hardcoded, toda migração é um bom investimento.

Perguntas frequentes

O que é uma CSS custom property (variável CSS)?+

CSS custom property é uma variável declarada no CSS com prefixo '--' e usada via função var(). Exemplo: --primary: #3B82F6; e depois color: var(--primary). Diferente de variáveis de SASS/LESS (que são resolvidas no build), custom properties do CSS nativo são dinâmicas: podem mudar em tempo real, responder a media queries e ser alteradas via JavaScript. Essa dinamismo é o que viabiliza dark mode e theming moderno.

Devo usar cores diretas ou sempre via variável?+

Via variável, sempre. Cor hardcoded (color: #3B82F6) em 50 lugares diferentes vira pesadelo de manutenção quando o brand muda. Com variável, você troca em um lugar só. Além disso, variável permite theming, dark mode e ajustes dinâmicos. A única exceção razoável: cores de debug, protótipo descartável ou gradiente específico que não faz sentido tokenizar.

Qual a diferença entre tokens primitivos e semânticos?+

Tokens primitivos são a paleta crua (--blue-500, --gray-100). Tokens semânticos dão significado de uso (--surface, --text-primary, --border). Semânticos apontam para primitivos: --surface: var(--gray-50). Separar os dois níveis permite mudar primitivos (rebrand) sem mexer em componentes, e mudar tokens semânticos (dark mode) sem alterar a paleta. É o padrão de design system maduro.

Preciso de escala 50-950 como Tailwind?+

Não obrigatório, mas é convenção útil. 10 variações por matiz cobrem background, hover, text, border e dark mode com sobras. Design systems próprios geralmente usam 5-10 steps; alguns (Radix Colors) usam 12. Escala de 10 é o sweet spot entre flexibilidade e simplicidade. Começa em 50 (quase branco) e vai até 950 (quase preto), com 500 como cor base.

Como implementar dark mode com CSS variables?+

Defina tokens semânticos no :root (modo claro) e sobrescreva em media query ou seletor de tema. Exemplo: [data-theme='dark'] { --surface: var(--gray-900); --text: var(--gray-100); }. Componentes que usam var(--surface) automaticamente adaptam. Também funciona com prefers-color-scheme: @media (prefers-color-scheme: dark). Dark mode moderno é quase sempre feito assim.

CSS variables funcionam em todos os navegadores?+

Em 2026, sim — em todos os navegadores modernos. Suporte universal desde 2017 (caniuse 98%+). IE11 não suporta, mas IE11 está morto (suporte oficial acabou em 2022). Para fallback, você pode declarar a cor diretamente antes da variável: color: #3B82F6; color: var(--primary); — navegadores antigos ignoram a linha do var().

Vale a pena usar OKLCH em vez de HEX em design system?+

Sim, em projetos novos. OKLCH é perceptualmente uniforme: variações de lightness geram mudanças visuais consistentes em qualquer matiz. Tailwind v4, shadcn/ui e design systems modernos migraram pra OKLCH em 2024-2025. Ganha em gradientes harmoniosos, suporte a wide gamut (P3) e uniformidade entre cores claras e escuras. Custo: ferramentas visuais ainda mostram HEX com mais frequência.

Como gerar uma paleta harmoniosa a partir de uma cor base?+

Três caminhos. (1) Ferramentas visuais: Tailwind UI Color Palette Generator, Radix Colors, uicolors.app — inseriu a cor base, sai a escala completa. (2) OKLCH programático: fixar H e C, variar L em steps geométricos. (3) Manual com HSL: fixar H, ajustar L em incrementos de 10%. A primeira é a mais rápida; a segunda entrega melhor consistência visual em 2026.

#paleta de cores#css variables#custom properties#design system#dark mode#tokens#tailwind#shadcn#oklch

Artigos relacionados