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:
@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.
: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:
@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:
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.