Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

Cores no Tailwind: a escala 50–950, opacidade e oklch

Como funciona a escala de 50 a 950, o que muda no v4 com oklch, como aplicar opacidade com a barra e como colocar a cor da sua marca dentro da paleta.

Rodolfo Mori10 min de leitura

No Tailwind você não escolhe uma cor: você escolhe uma família (sky, rose, slate) e um passo dentro dela, de 50 a 950. bg-sky-50 é um azul quase branco; bg-sky-950 é quase preto. O número não é intensidade de cor — é o inverso da luminosidade.

Todos os exemplos aqui são o painel de atendimentos de uma clínica veterinária, a Patinha Feliz: fichas de paciente, etiquetas de status e a cor da marca. E todo CSS que aparece abaixo dos blocos saiu do compilador de verdade, com este comando:

bash
npx @tailwindcss/cli -i in.css -o out.css --content painel.html
≈ tailwindcss v4.3.3

Done in 25ms

Se você ainda não instalou nada, comece por como instalar o Tailwind CSS v4 e volte para cá — o resto desta lição depende de você conseguir compilar.

Ler a escala: 50 é quase branco, 950 é quase preto

A versão 4.3.3 traz 26 famílias de cor com 11 passos cada — 286 cores nomeadas, todas escritas em oklch(). Esta é a família sky inteira, copiada do theme.css que vem dentro do pacote:

css
--color-sky-50:  oklch(97.7% 0.013 236.62);
--color-sky-100: oklch(95.1% 0.026 236.824);
--color-sky-200: oklch(90.1% 0.058 230.902);
--color-sky-300: oklch(82.8% 0.111 230.318);
--color-sky-400: oklch(74.6% 0.16 232.661);
--color-sky-500: oklch(68.5% 0.169 237.323);
--color-sky-600: oklch(58.8% 0.158 241.966);
--color-sky-700: oklch(50% 0.134 242.749);
--color-sky-800: oklch(44.3% 0.11 240.79);
--color-sky-900: oklch(39.1% 0.09 240.876);
--color-sky-950: oklch(29.3% 0.066 243.157);

O primeiro número de cada linha é a luminosidade. Ele cai de 97,7% até 29,3% conforme o passo sobe. O terceiro número, o matiz, quase não se move: fica girando na casa dos 230 a 243. É por isso que a família parece uma cor só, em onze intensidades.

Na prática do dia a dia, três faixas resolvem quase tudo:

faixa serve para exemplo na clínica
50–200 fundo de área, fundo de etiqueta, divisória fundo do cartão do paciente
300–500 ícone decorativo, borda visível, gráfico barrinha de espécie na lateral
600–950 texto, botão sólido, título nome do paciente, botão “Chamar”

bg-, text-, border-, ring-: a cor é sempre o sufixo

O nome da classe é sempre o mesmo padrão: o que você quer pintar, depois a cor. Isso vale para todas as propriedades que aceitam cor.

html
<article class="bg-sky-50 border-slate-200 text-slate-900">
  <span class="bg-emerald-100 text-emerald-800">Alta</span>
  <span class="bg-amber-100 text-amber-800">Em atendimento</span>
  <span class="bg-rose-600 text-white">Urgente</span>
  <p class="text-slate-500">Rex, 4 anos</p>
</article>

O compilador transforma cada classe numa regra de uma linha só, apontando para a variável da cor:

.border-slate-200 { border-color: var(--color-slate-200); } .bg-amber-100 { background-color: var(--color-amber-100); } .bg-emerald-100 { background-color: var(--color-emerald-100); } .bg-rose-600 { background-color: var(--color-rose-600); } .bg-sky-50 { background-color: var(--color-sky-50); } .text-amber-800 { color: var(--color-amber-800); } .text-emerald-800{ color: var(--color-emerald-800); } .text-slate-500 { color: var(--color-slate-500); } .text-slate-900 { color: var(--color-slate-900); } .text-white { color: var(--color-white); }

Repare no que não apareceu: nenhuma das outras 276 cores. O v4 só emite a variável da cor que ele encontrou no seu código. A paleta inteira não pesa no arquivo final — pesa só o que você usou. É a mesma lógica que vale para espaçamento e para qualquer outro utilitário.

Gradiente segue o mesmo padrão, com from-, via- e to-:

html
<div class="bg-linear-to-r from-sky-600 to-emerald-500"></div>
.bg-linear-to-r { --tw-gradient-position: to right; @supports (background-image: linear-gradient(in lab, red, red)) { --tw-gradient-position: to right in oklab; } background-image: linear-gradient(var(--tw-gradient-stops)); } .from-sky-600 { --tw-gradient-from: var(--color-sky-600); … } .to-emerald-500 { --tw-gradient-to: var(--color-emerald-500); … }

O que muda quando o v4 troca rgb por oklch

Na versão 3, a paleta era escrita em RGB. Na 4, é oklch(). A diferença que você sente não é a cor em si — é o comportamento nas bordas.

oklch() separa luminosidade, croma e matiz em três números independentes. Isso deixa o Tailwind girar o matiz sem que a cor mude de brilho, e é o que permite o navegador exibir tons mais vivos em telas modernas, que enxergam além do sRGB. A gramática de oklch() segue a especificação moderna de cores no CSS; aqui o foco permanece no modo como a paleta Tailwind usa esses valores.

O que quase ninguém conta é que a escala não é uniforme entre famílias. Medi a luminosidade de cada passo em seis famílias, lendo os valores direto do theme.css:

js
import Color from 'colorjs.io';
import { readFileSync } from 'node:fs';

const tema = readFileSync('node_modules/tailwindcss/theme.css', 'utf8');
const cor = (n) =>
  new Color(tema.match(new RegExp(`--color-${n}:\\s*([^;]+);`))[1].trim());

for (const passo of [400, 500, 600, 700]) {
  const linha = ['sky', 'emerald', 'amber', 'rose', 'violet', 'slate']
    .map((f) => (cor(`${f}-${passo}`).oklch.l * 100).toFixed(1).padStart(7));
  console.log(String(passo).padEnd(6) + linha.join(''));
}
400 74.6 76.5 82.8 71.2 70.2 70.4 500 68.5 69.6 76.9 64.5 60.6 55.4 600 58.8 59.6 66.6 58.6 54.1 44.6 700 50.0 50.8 55.5 51.4 49.1 37.2

No passo 600, amber está com 66,6% de luminosidade e slate com 44,6%. São 22 pontos de diferença sob o mesmo número. Trocar text-sky-600 por text-amber-600 porque “é o mesmo passo” muda o contraste da página inteira.

Opacidade com barra: bg-slate-900/40 no lugar de bg-opacity-40

Para deixar uma cor transparente, cole uma barra e o percentual no fim da classe. Serve para qualquer utilitário de cor.

html
<div class="bg-slate-900/40"></div>
.bg-slate-900\/40 { background-color: color-mix(in srgb, oklch(20.8% 0.042 265.755) 40%, transparent); @supports (color: color-mix(in lab, red, red)) { background-color: color-mix(in oklab, var(--color-slate-900) 40%, transparent); } }

Aqui está a mudança de mecanismo que o v4 trouxe. Não sai um rgba() com quarto canal: sai um color-mix(), misturando a cor com transparent no espaço oklab. E sai duas vezes — a primeira linha, em srgb, é a versão de reserva para navegador que não entende color-mix(in lab, …); a segunda, dentro do @supports, é a que os navegadores atuais aplicam.

A consequência prática é boa: como o valor é um color-mix em cima de var(--color-slate-900), trocar o valor da variável no modo escuro muda a versão transparente junto, sem você escrever nada.

html
<div class="bg-slate-900 bg-opacity-40 text-white text-opacity-70"></div>
.bg-slate-900 { background-color: var(--color-slate-900); } .text-white { color: var(--color-white); }

O fundo continua opaco, o compilador termina com “Done in 23ms” e ninguém avisa nada. Se você migrou um projeto da versão 3 e a transparência sumiu, o culpado é este. Outras causas de classe que não pega estão reunidas em Tailwind não está aplicando as classes.

O par texto/fundo que passa no contraste — medido

A WCAG pede razão de contraste de 4,5:1 para texto normal e 3:1 para texto grande (a partir de 18,66px em negrito ou 24px). Medi os onze passos de sky como texto sobre fundo branco, com a fórmula da WCAG 2.1:

js
const branco = new Color('#fff');

for (const passo of [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]) {
  const razao = branco.contrastWCAG21(cor(`sky-${passo}`));
  console.log(
    `sky-${String(passo).padEnd(3)} | ${razao.toFixed(2).padStart(5)}:1 | ` +
      `normal: ${razao >= 4.5 ? 'passa' : 'REPROVA'} | ` +
      `grande: ${razao >= 3 ? 'passa' : 'REPROVA'}`
  );
}
sky-50 | 1.07:1 | normal: REPROVA | grande: REPROVA sky-100 | 1.15:1 | normal: REPROVA | grande: REPROVA sky-200 | 1.33:1 | normal: REPROVA | grande: REPROVA sky-300 | 1.66:1 | normal: REPROVA | grande: REPROVA sky-400 | 2.20:1 | normal: REPROVA | grande: REPROVA sky-500 | 2.77:1 | normal: REPROVA | grande: REPROVA sky-600 | 4.09:1 | normal: REPROVA | grande: passa sky-700 | 5.94:1 | normal: passa | grande: passa sky-800 | 7.58:1 | normal: passa | grande: passa sky-900 | 9.47:1 | normal: passa | grande: passa sky-950 | 13.90:1 | normal: passa | grande: passa

Guarde a linha do 600. text-sky-600 sobre branco dá 4,09:1 — reprova por pouco no texto de tamanho normal, e é exatamente a classe que todo mundo usa em link. O primeiro passo seguro para texto corrido é o 700.

E o “reprova por pouco” muda de família para família. Medindo o passo 600 de cada uma sobre branco: amber-600 dá 3,21:1, emerald-600 dá 3,74:1, sky-600 dá 4,09:1, rose-600 dá 4,78:1, violet-600 dá 5,88:1 e slate-600 dá 7,56:1. Só as três últimas passam.

Agora as combinações que a clínica usa de verdade nas etiquetas:

status classes razão veredito
Alta bg-emerald-100 text-emerald-800 6,73:1 passa
Alta bg-emerald-100 text-emerald-600 3,29:1 reprova
Em atendimento bg-amber-100 text-amber-800 6,41:1 passa
Em atendimento bg-amber-100 text-amber-600 2,88:1 reprova
Urgente bg-rose-600 text-white 4,78:1 passa
Urgente bg-rose-500 text-white 3,73:1 reprova
Agendado bg-sky-100 text-sky-800 6,59:1 passa
Agendado bg-sky-600 text-white 4,09:1 reprova

Daí sai uma regra que você consegue aplicar de cabeça: para etiqueta clara, conte seis passos de distância. Fundo 100 com texto 700 ou 800; fundo 200 com texto 800. Fundo 100 com texto 600 são cinco passos, e cinco passos não bastam.

O erro mais comum: a classe que o Tailwind nunca viu

Este é o tropeço número um com cores no Tailwind, e ele custa uma tarde. A etiqueta de status parece pedir uma classe montada na hora:

jsx
const CORES = { alta: 'emerald', atendimento: 'amber', urgente: 'rose' };

export function Etiqueta({ status, texto }) {
  const cor = CORES[status];
  return (
    <span className={`bg-${cor}-100 text-${cor}-800 rounded-full px-3 py-1`}>
      {texto}
    </span>
  );
}
.rounded-full { border-radius: calc(infinity * 1px); } .px-3 { padding-inline: calc(var(--spacing) * 3); } .py-1 { padding-block: var(--spacing); }

rounded-full, px-3 e py-1 foram gerados. As seis classes de cor, não. O Tailwind não executa o seu JavaScript: ele lê o arquivo como texto e procura por sequências que pareçam nomes de classe. bg-${cor}-100 nunca é um nome de classe no arquivo — é um pedaço de template.

A correção é guardar a string inteira, e não o pedaço:

jsx
const ESTILO = {
  alta: 'bg-emerald-100 text-emerald-800',
  atendimento: 'bg-amber-100 text-amber-800',
  urgente: 'bg-rose-600 text-white',
};

export function Etiqueta({ status, texto }) {
  return (
    <span className={`${ESTILO[status]} rounded-full px-3 py-1`}>{texto}</span>
  );
}
.bg-amber-100 { background-color: var(--color-amber-100); } .bg-emerald-100 { background-color: var(--color-emerald-100); } .bg-rose-600 { background-color: var(--color-rose-600); } .text-amber-800 { color: var(--color-amber-800); } .text-emerald-800 { color: var(--color-emerald-800); } .text-white { color: var(--color-white); }

O componente ficou até mais legível: o mapa agora diz qual é a aparência de cada status, em vez de esconder metade dela na interpolação.

Cor arbitrária, cor de variável e cor vinda da API

Quando a cor não está na paleta, existem três saídas, em ordem de preferência.

A primeira é o valor arbitrário, entre colchetes. Serve para um caso isolado:

html
<div class="bg-[#0d9488] text-[oklch(97%_0.02_190)]"></div>
.bg-\[\#0d9488\] { background-color: #0d9488; } .text-\[oklch\(97\%_0\.02_190\)\] { color: oklch(97% 0.02 190); }

Repare no sublinhado no lugar do espaço dentro do oklch(). O nome da classe não pode ter espaço, então o Tailwind usa _ e converte de volta na saída.

A segunda é apontar para uma variável CSS, com parênteses. É o atalho do v4 para var(--nome), e resolve o caso em que a cor só existe em tempo de execução — a espécie do paciente chega do banco de dados:

jsx
export function Ficha({ paciente }) {
  return (
    <article
      className="border-l-4 bg-white p-4"
      style={{ '--cor-especie': paciente.corDaEspecie }}
    >
      <span className="bg-(--cor-especie)/15 text-(--cor-especie) rounded px-2">
        {paciente.especie}
      </span>
    </article>
  );
}
.bg-\(--cor-especie\)\/15 { background-color: var(--cor-especie); @supports (color: color-mix(in lab, red, red)) { background-color: color-mix(in oklab, var(--cor-especie) 15%, transparent); } } .text-\(--cor-especie\) { color: var(--cor-especie); }

Duas coisas acontecem aí, e as duas são úteis. O valor da cor sai do CSS e vai para o atributo style, onde pode ser qualquer coisa. E a barra de opacidade continua funcionando em cima da variável, porque color-mix() aceita var() como argumento. Se as variáveis CSS ainda são novidade para você, é o momento de conhecer.

A terceira saída é a boa para cor que se repete: dar um nome a ela.

Colocar a cor da clínica dentro da escala

O bloco @theme registra cores novas no mesmo lugar de onde vêm as originais. O nome precisa começar com --color-, e o que vier depois vira o sufixo da classe:

css
@import "tailwindcss";

@theme {
  --color-patinha-100: oklch(93.5% 0.045 190);
  --color-patinha-500: oklch(70% 0.132 190);
  --color-patinha-600: oklch(60% 0.118 190);
  --color-patinha-800: oklch(43% 0.082 190);
}

Com isso no arquivo de entrada, bg-patinha-600, text-patinha-800 e até ring-patinha-500/30 passam a existir, com o mesmo comportamento das cores de fábrica:

.bg-patinha-600 { background-color: var(--color-patinha-600); } .text-patinha-800 { color: var(--color-patinha-800); } .ring-patinha-500\/30 { --tw-ring-color: color-mix(in srgb, oklch(70% 0.132 190) 30%, transparent); @supports (color: color-mix(in lab, red, red)) { --tw-ring-color: color-mix(in oklab, var(--color-patinha-500) 30%, transparent); } }

Note que declarei quatro passos e usei quatro. Você não precisa preencher os onze de cara — declare o que a interface usa hoje.

O prefixo é literal, e errar nele custa caro porque não sai erro nenhum. Escrevendo --cor-patinha-600 em vez de --color-patinha-600, o resultado é este:

css
@theme {
  --cor-patinha-600: oklch(60% 0.118 190);
}
@layer utilities;

Uma camada de utilitários vazia, o compilador satisfeito e a página sem cor nenhuma. O @theme não valida nomes: variável que não começa com um prefixo conhecido é tratada como variável comum e ignorada pelo gerador de classes.

Num design system a sério, vale ir além e apagar a paleta de fábrica, para que ninguém no time use bg-sky-500 por engano:

css
@theme {
  --color-*: initial;
  --color-white: #fff;
  --color-patinha-100: oklch(93.5% 0.045 190);
  --color-patinha-600: oklch(60% 0.118 190);
  --color-patinha-800: oklch(43% 0.082 190);
}
.bg-patinha-100 { background-color: var(--color-patinha-100); } .text-patinha-800 { color: var(--color-patinha-800); }

O HTML de teste tinha bg-sky-500 junto das outras classes. Ele simplesmente não existe mais. É o tipo de trava que transforma uma paleta em regra, e não em sugestão.

O que vem depois

Você já consegue montar um painel inteiro com cor consistente e contraste verificado. O passo seguinte é dar nome também à tipografia e às escalas da marca, e isso é assunto de @theme no Tailwind v4. Se preferir revisar a base antes, o que é o Tailwind explica como uma classe vira CSS, e o guia completo de Tailwind mostra a ordem de estudo até o fim da trilha.

Antes de seguir, faça um teste no seu projeto: procure todo text- seguido de um passo 600 ou menor aplicado a texto de leitura. Pela tabela acima, boa parte deles está abaixo de 4,5:1.

Prefere aprender em vídeo?

Tem aula sobre este assunto no nosso canal.

Ver todos os vídeos do canal
  • tailwind
  • cores
  • oklch
  • paleta
  • contraste
  • design system

Perguntas frequentes

Existe passo 1000 ou passo 25 na escala do Tailwind?
Não. A escala do v4.3.3 tem exatamente onze passos por família — 50, 100, 200, 300, 400, 500, 600, 700, 800, 900 e 950 — em 26 famílias, o que dá 286 cores nomeadas. Se você precisa de um tom entre dois passos, crie ele no bloco @theme com o nome que quiser.
Preciso decorar quais cores existem?
Não. Você decora o padrão, que é sempre o mesmo — prefixo da propriedade, hífen, nome da família, hífen, passo. Sabendo que existe bg-rose-600, você já sabe escrever text-rose-600, border-rose-600 e ring-rose-600 sem consultar nada.
Dá para usar cor em hex nas classes do Tailwind?
Dá, com a sintaxe de colchete, como bg-[#0d9488]. Vale para um caso isolado. Se a mesma cor aparece em três lugares do projeto, ela merece um nome no @theme — cor sem nome vira cor duplicada com um dígito trocado.
A opacidade com barra funciona em cima de variável CSS?
Funciona. bg-(--cor-especie)/15 gera um color-mix em cima do valor da variável, o que permite receber a cor do servidor em tempo de execução e ainda assim controlar a transparência por classe.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em @tailwindcss/cli 4.3.3 no Node 24.16.0, com colorjs.io 0.6.0, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. Tailwind CSS — Colors — tailwindcss.com
  2. MDN — color-mix() — developer.mozilla.org
  3. WCAG 2.2 — Contrast (Minimum), critério 1.4.3 — w3.org

Continue por aqui