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).
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.
| Critério | Definição | Exemplo |
|---|---|---|
| Primitivos | Paleta crua, cores cruas | --blue-500, --gray-100, --red-700 |
| Semânticos | Uso contextual, apontam para primitivos | --surface, --text-primary, --border |
Contexto
--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
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
--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
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
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).
| Critério | Par | Contraste mínimo |
|---|---|---|
| --text-primary em --surface | Ex: gray-900 em gray-50 | 4.5:1 |
| --text-secondary em --surface | gray-600 em gray-50 | 4.5:1 |
| --primary-foreground em --primary | white em blue-600 | 4.5:1 |
| --text-disabled em --surface | gray-400 em gray-50 | 3:1 (texto grande) |
| --border em --surface | Não precisa | N/A (elemento não-texto) |
Atenção
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-mediumvira 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
--primaryno:rootdentro 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:
- Audite: grep de
#[0-9a-fA-F]{3,6}ergb(. Levante todas as cores únicas. - Consolide em primitivos: 27 azuis diferentes viram 10 steps da escala blue.
- Crie semânticos: mapeie uso → semântico → primitivo.
- Substitua por componente: componente por componente, troque HEX por var().
- Adicione dark mode: agora que há camada semântica, dark vira override.
Vai mais fundo
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.
Artigos relacionados
Cores na Web (2026): HEX, RGB, HSL, OKLCH e Quando Usar Cada Um
Guia completo de sistemas de cor na web: HEX, RGB, HSL, OKLCH, transparência, conversão entre formatos e qual escolher em cada situação.
Contraste de Cores e Acessibilidade Web: Guia WCAG 2.2 Completo (2026)
Como aplicar contraste WCAG AA e AAA: fórmula de luminância, exceções de texto grande, WCAG 3 APCA, testes em dark mode, integração no design system, ferramentas e auditoria automática.