Diário do Projeto
rafazingano.com.br

88 design tokens, do JavaScript para o CSS

Migrar o tema do `tailwind.config.js` para o bloco `@theme` do Tailwind 4 não foi só troca de sintaxe: transformou cada token numa custom property, o que deixou o tema escuro pronto para ser ligado sem tocar em componente nenhum.

Performance Baseline

Tokens migrados
88
CSS final (gzip)
11.6 KB

Os nove layouts carregavam o Tailwind por CDN e traziam a configuração inteira inline, num <script> de 90 linhas repetido em cada arquivo. Paleta Material Design 3 completa, escala tipográfica, espaçamento — tudo duplicado nove vezes.

No Tailwind 4 isso vira CSS. E a mudança é mais profunda do que parece.

@theme não é theme.extend com outra sintaxe

No Tailwind 3, a configuração vivia em JavaScript e era consumida no build para gerar as classes. O valor final existia só como número dentro do CSS gerado.

No Tailwind 4, cada token vira uma custom property publicada no documento:

src/styles/global.css
@import 'tailwindcss';

@theme {
  --color-surface: #f8f9ff;
  --color-on-surface: #0b1c30;
  --color-primary: #004ac6;
  --color-outline-variant: #c3c6d7;

  --font-display: 'JetBrains Mono Variable', ui-monospace, monospace;
  --font-body: 'Inter Variable', system-ui, sans-serif;

  --text-display-lg: 48px;
  --text-display-lg--line-height: 1.1;
  --text-display-lg--letter-spacing: -0.02em;
  --text-display-lg--font-weight: 700;

  --spacing-gutter: 24px;
  --spacing-section: 80px;
  --container-page: 1120px;

  --shadow-ambient: 0 4px 15px rgb(86 94 116 / 0.04);
}

Isso gera bg-surface, text-on-surface, font-display, text-display-lg, px-gutter, py-section, max-w-page e shadow-ambient — mas o valor continua acessível em runtime como variável CSS.

A consequência prática: o tema escuro deixa de ser um problema de build.

O que basta acrescentar quando eu ligar o dark mode
:root[data-theme='dark'] {
  --color-surface: #111318;
  --color-on-surface: #e2e2e9;
  --color-primary: #b4c5ff;
}

Nenhum componente muda. Nenhuma classe dark: é escrita. As 88 declarações já apontam para as variáveis certas.

A armadilha que me custou meia hora

Defini --container-artigo: 800px para a largura dos posts. Só que primeiro eu tinha chamado de --container-prose, e o CSS gerado saiu assim:

.max-w-prose { max-width: 65ch }

O max-w-prose já existe no Tailwind como utilitário fixo de 65ch, e o utilitário nativo vence o token do @theme. O post ficava 180px mais estreito que o layout aprovado e eu não entendia por quê.

A lição: antes de nomear um token, confira se o utilitário resultante já existe. Renomeei para --container-artigo e resolveu.

Onde os valores divergiam

O DESIGN.md e o tailwind.config dos HTMLs discordavam no raio de borda — o documento dizia 0.5rem como padrão, o código dizia 0.25rem. Adotei a regra de que vale o que efetivamente renderiza, então o código ganhou.

Detalhe feliz: os valores dos layouts (0.25rem, lg: 0.5rem, xl: 0.75rem) são exatamente os padrões do Tailwind 4. Não precisei declarar raio nenhum.

Utilitários próprios

Três efeitos apareciam repetidos como CSS solto nos layouts. Viraram utilitário de verdade com @utility, o que permite combiná-los com variantes:

src/styles/global.css
@utility hover-lift {
  transition:
    transform 0.2s ease-in-out,
    box-shadow 0.2s ease-in-out;

  &:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-lift);
  }
}

@utility bg-grid {
  background-size: 40px 40px;
  background-image:
    linear-gradient(to right, rgb(226 232 240 / 0.5) 1px, transparent 1px),
    linear-gradient(to bottom, rgb(226 232 240 / 0.5) 1px, transparent 1px);
}

Sobre a integração @astrojs/tailwind

Não usei, e é intencional. Com o Tailwind 4 o caminho é o plugin do Vite:

astro.config.mjs
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  vite: { plugins: [tailwindcss()] },
});

Uma linha, sem camada intermediária.

O número final

53,8 KB de CSS, 11,6 KB depois do gzip, para o site inteiro — todas as páginas, todos os componentes, todos os 88 tokens. É o único arquivo de estilo que o navegador baixa, e ele fica em cache para sempre.

Uma boa parte desses 53 KB são as próprias declarações de custom property, que comprimem muito bem por serem repetitivas. Foi um preço barato pelo tema escuro que ainda vou ligar.

Tailwind CSSDesign SystemCSS