Tailwind CSS v4: O Que Realmente Mudou e Se Vale Migrar
Configuração CSS-first, integração com @layer, container queries nativas, performance do novo engine, breaking changes e minha experiência honesta de migração do v3 para o v4.
Uso o Tailwind CSS desde a v1.x, quando metade da comunidade achava que era uma aberração e a outra metade não parava de fazer deploy com ele. Cada versão major foi um salto significativo, mas a v4 é diferente. Não é apenas um release de features. É uma reescrita arquitetural do zero que muda o contrato fundamental entre você e o framework.
Depois de migrar dois projetos de produção da v3 para a v4 e começar três projetos novos na v4 do zero, tenho uma visão clara do que é genuinamente melhor, do que ainda está bruto e se você deveria migrar hoje. Sem hype, sem indignação — apenas o que observei.
O Panorama Geral: O Que a v4 Realmente É#
O Tailwind CSS v4 são três coisas ao mesmo tempo:
- Um novo engine — reescrito de JavaScript para Rust (o engine Oxide), tornando os builds dramaticamente mais rápidos
- Um novo paradigma de configuração — configuração CSS-first substitui o
tailwind.config.jscomo padrão - Uma integração mais estreita com a plataforma CSS —
@layernativo, container queries,@starting-stylee cascade layers são cidadãos de primeira classe
A manchete que você vai ver em todo lugar é "10x mais rápido." Isso é real, mas subestima a mudança de verdade. O modelo mental para configurar e estender o Tailwind mudou fundamentalmente. Você está trabalhando com CSS agora, não com um objeto de configuração JavaScript que gera CSS.
Veja como um setup mínimo do Tailwind v4 se parece:
/* app.css — este é o setup inteiro */
@import "tailwindcss";É isso. Sem arquivo de config. Sem configuração de plugin PostCSS (para a maioria dos setups). Sem as diretivas @tailwind base; @tailwind components; @tailwind utilities;. Um import, e você está rodando.
Compare com a v3:
/* v3 — app.css */
@tailwind base;
@tailwind components;
@tailwind utilities;// v3 — tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {},
},
plugins: [],
};// v3 — postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};Três arquivos reduzidos a uma linha. Não é apenas menos boilerplate — é menos superfície para configuração errada. Na v4, a detecção de conteúdo é automática. Ela escaneia os arquivos do seu projeto sem precisar que você descreva padrões glob.
Configuração CSS-First com @theme#
Esta é a maior mudança conceitual. Na v3, você customizava o Tailwind através de um objeto de config JavaScript:
// v3 — tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
brand: {
50: "#eff6ff",
500: "#3b82f6",
900: "#1e3a5f",
},
},
fontFamily: {
display: ["Inter Variable", "sans-serif"],
},
spacing: {
18: "4.5rem",
112: "28rem",
},
borderRadius: {
"4xl": "2rem",
},
},
},
};Na v4, tudo isso vive no CSS usando a diretiva @theme:
@import "tailwindcss";
@theme {
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
--font-display: "Inter Variable", sans-serif;
--spacing-18: 4.5rem;
--spacing-112: 28rem;
--radius-4xl: 2rem;
}No início, eu resisti a isso. Gostava de ter um único objeto JavaScript onde podia ver todo o meu design system. Mas depois de uma semana com a abordagem CSS, mudei de ideia por três razões:
1. As custom properties CSS nativas são expostas automaticamente. Todo valor que você define no @theme se torna uma custom property CSS no :root. Isso significa que os valores do seu tema ficam acessíveis em CSS puro, em CSS Modules, em tags <style>, em qualquer lugar onde CSS funciona:
/* você ganha isso de graça */
:root {
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}/* use em qualquer lugar — sem precisar do Tailwind */
.custom-element {
border: 2px solid var(--color-brand-500);
}2. Você pode usar funcionalidades CSS dentro do @theme. Media queries, light-dark(), calc() — CSS real funciona aqui porque é CSS real:
@theme {
--color-surface: light-dark(#ffffff, #0a0a0a);
--color-text: light-dark(#0a0a0a, #fafafa);
--spacing-container: calc(100vw - 2rem);
}3. Colocalização com o resto do seu CSS. Seu tema, seus utilitários customizados e seus estilos base vivem na mesma linguagem, no mesmo arquivo se quiser. Não há alternância de contexto entre "mundo CSS" e "mundo de config JavaScript."
Sobrescrevendo vs. Estendendo o Tema Padrão#
Na v3 você tinha theme (substituir) vs theme.extend (mesclar). Na v4, o modelo mental é diferente:
@import "tailwindcss";
/* Isso ESTENDE o tema padrão — adiciona cores brand junto com as existentes */
@theme {
--color-brand-500: #3b82f6;
}Se você quer substituir completamente um namespace (como remover todas as cores padrão), use @theme com o reset wildcard --color-*:
@import "tailwindcss";
@theme {
/* Limpa todas as cores padrão primeiro */
--color-*: initial;
/* Agora defina apenas suas cores */
--color-white: #ffffff;
--color-black: #000000;
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}Esse padrão de reset com wildcard é elegante. Você escolhe exatamente quais partes do tema padrão manter e quais substituir. Quer todo o spacing padrão mas cores customizadas? Resete --color-*: initial; e deixe o spacing intacto.
Múltiplos Arquivos de Tema#
Para projetos maiores, você pode dividir seu tema em arquivos:
/* styles/theme/colors.css */
@theme {
--color-brand-50: #eff6ff;
--color-brand-100: #dbeafe;
--color-brand-200: #bfdbfe;
--color-brand-300: #93c5fd;
--color-brand-400: #60a5fa;
--color-brand-500: #3b82f6;
--color-brand-600: #2563eb;
--color-brand-700: #1d4ed8;
--color-brand-800: #1e40af;
--color-brand-900: #1e3a5f;
--color-brand-950: #172554;
}
/* styles/theme/typography.css */
@theme {
--font-display: "Inter Variable", sans-serif;
--font-body: "Source Sans 3 Variable", sans-serif;
--font-mono: "JetBrains Mono Variable", monospace;
--text-display: 3.5rem;
--text-display--line-height: 1.1;
--text-display--letter-spacing: -0.02em;
}/* app.css */
@import "tailwindcss";
@import "./theme/colors.css";
@import "./theme/typography.css";Isso é muito mais limpo que o padrão da v3 de ter um tailwind.config.js gigante ou tentar dividi-lo com require().
O Engine Oxide: É Realmente 10x Mais Rápido#
O engine do Tailwind v4 é uma reescrita completa em Rust. Eles chamam de Oxide. Eu era cético quanto à afirmação "10x mais rápido" — números de marketing raramente sobrevivem ao contato com projetos reais. Então fiz benchmarks.
Meu projeto de teste: Um app Next.js com 847 componentes, 142 páginas, aproximadamente 23.000 usos de classes Tailwind.
| Métrica | v3 (Node) | v4 (Oxide) | Melhoria |
|---|---|---|---|
| Build inicial | 4.280ms | 387ms | 11x |
| Incremental (editar 1 arquivo) | 340ms | 18ms | 19x |
| Rebuild completo (limpo) | 5.100ms | 510ms | 10x |
| Início do dev server | 3.200ms | 290ms | 11x |
A afirmação "10x" é conservadora para o meu projeto. Builds incrementais são onde ele realmente brilha — 18ms significa que é essencialmente instantâneo. Você salva um arquivo e o navegador já tem os novos estilos antes de você trocar de aba.
Por Que É Tão Mais Rápido?#
Três razões:
1. Rust ao invés de JavaScript. O parser CSS core, detecção de classes e geração de código são todos em Rust nativo. Isso não é uma situação "vamos reescrever em Rust por diversão" — parsing CSS é genuinamente trabalho CPU-bound onde código nativo tem uma vantagem massiva sobre o V8.
2. Sem PostCSS no caminho crítico. Na v3, o Tailwind era um plugin PostCSS. Todo build significava: parsear CSS para AST do PostCSS, executar plugin do Tailwind, serializar de volta para string CSS, depois outros plugins PostCSS rodam. Na v4, o Tailwind tem seu próprio parser CSS que vai direto da fonte para a saída. O PostCSS ainda é suportado para compatibilidade, mas o caminho primário o ignora completamente.
3. Processamento incremental mais inteligente. O novo engine faz cache agressivamente. Quando você edita um único arquivo, ele só re-escaneia aquele arquivo em busca de nomes de classe e só regenera as regras CSS que mudaram. O engine da v3 era mais esperto nisso do que as pessoas reconhecem (o modo JIT já era incremental), mas a v4 leva muito mais longe com rastreamento de dependências de granularidade fina.
A Velocidade Realmente Importa?#
Sim, mas não pela razão que você esperaria. Para a maioria dos projetos, a velocidade de build da v3 era "ok." Você esperava alguns centenas de milissegundos no dev. Não era doloroso.
A velocidade da v4 importa porque torna o Tailwind invisível no seu toolchain. Quando os builds são abaixo de 20ms, você para de pensar no Tailwind como um passo de build. Ele se torna como syntax highlighting — sempre lá, nunca atrapalhando. Essa diferença psicológica é significativa ao longo de um dia inteiro de desenvolvimento.
Integração Nativa com @layer#
Na v3, o Tailwind usava seu próprio sistema de layers com @layer base, @layer components e @layer utilities. Eles pareciam cascade layers CSS mas não eram — eram diretivas específicas do Tailwind que controlavam onde o CSS gerado aparecia na saída.
Na v4, o Tailwind usa cascade layers CSS reais:
/* saída da v4 — simplificada */
@layer theme, base, components, utilities;
@layer base {
/* reset, preflight */
}
@layer components {
/* suas classes de componentes */
}
@layer utilities {
/* todas as classes utilitárias geradas */
}Isso é uma mudança significativa porque cascade layers CSS têm implicações reais de especificidade. Uma regra em um layer de menor prioridade sempre perde para uma regra em um layer de maior prioridade, independentemente da especificidade do seletor. Isso significa:
@layer components {
/* especificidade: 0-1-0 */
.card { padding: 1rem; }
}
@layer utilities {
/* especificidade: 0-1-0 — mesma especificidade mas vence porque o layer utilities vem depois */
.p-4 { padding: 1rem; }
}Utilities sempre sobrescrevem components. Components sempre sobrescrevem base. É assim que o Tailwind funcionava conceitualmente na v3, mas agora é aplicado pelo mecanismo de cascade layers do navegador, não por manipulação de ordem de fonte.
Adicionando Utilities Customizados#
Na v3, você definia utilities customizados com uma API de plugin ou @layer utilities:
// v3 — abordagem de plugin
const plugin = require("tailwindcss/plugin");
module.exports = {
plugins: [
plugin(function ({ addUtilities }) {
addUtilities({
".text-balance": {
"text-wrap": "balance",
},
".text-pretty": {
"text-wrap": "pretty",
},
});
}),
],
};Na v4, utilities customizados são definidos com a diretiva @utility:
@import "tailwindcss";
@utility text-balance {
text-wrap: balance;
}
@utility text-pretty {
text-wrap: pretty;
}A diretiva @utility diz ao Tailwind "isso é uma classe utilitária — coloque no layer de utilities e permita que seja usada com variants." Essa última parte é fundamental. Um utilitário definido com @utility funciona automaticamente com hover:, focus:, md: e qualquer outra variant:
<p class="text-pretty md:text-balance">...</p>Variants Customizados#
Você também pode definir variants customizados com @variant:
@import "tailwindcss";
@variant hocus (&:hover, &:focus);
@variant theme-dark (.dark &);<button class="hocus:bg-brand-500 theme-dark:text-white">
Clique aqui
</button>Isso substitui a API de plugin addVariant da v3 para a maioria dos casos de uso. É menos poderoso (você não pode fazer geração programática de variants), mas cobre 90% do que as pessoas realmente fazem.
Container Queries: Nativo, Sem Plugin#
Container queries eram uma das features mais solicitadas na v3. Você podia obtê-las com o plugin @tailwindcss/container-queries, mas era um complemento. Na v4, são nativas do framework.
Uso Básico#
Marque um contêiner com @container e consulte seu tamanho com o prefixo @:
<!-- marca o pai como um contêiner -->
<div class="@container">
<!-- responsivo à largura do pai, não do viewport -->
<div class="flex flex-col @md:flex-row @lg:grid @lg:grid-cols-3">
<div class="p-4">Card 1</div>
<div class="p-4">Card 2</div>
<div class="p-4">Card 3</div>
</div>
</div>Os variants @md, @lg, etc. funcionam como breakpoints responsivos mas são relativos ao ancestral @container mais próximo ao invés do viewport. Os valores de breakpoint correspondem aos breakpoints padrão do Tailwind:
| Variant | Min-width |
|---|---|
@sm | 24rem (384px) |
@md | 28rem (448px) |
@lg | 32rem (512px) |
@xl | 36rem (576px) |
@2xl | 42rem (672px) |
Contêineres Nomeados#
Você pode nomear contêineres para consultar ancestrais específicos:
<div class="@container/sidebar">
<div class="@container/card">
<!-- consulta o contêiner do card -->
<div class="@md/card:text-lg">...</div>
<!-- consulta o contêiner da sidebar -->
<div class="@lg/sidebar:hidden">...</div>
</div>
</div>Por Que Isso Importa#
Container queries mudam como você pensa sobre design responsivo. Em vez de "nessa largura de viewport, mostre três colunas," você diz "quando o contêiner deste componente for largo o suficiente, mostre três colunas." Os componentes se tornam verdadeiramente autocontidos. Você pode mover um componente de card de um layout full-width para uma sidebar e ele se adapta automaticamente. Sem ginástica de media queries.
Tenho refatorado minhas bibliotecas de componentes para usar container queries por padrão ao invés de breakpoints de viewport. O resultado são componentes que funcionam em qualquer lugar onde você os coloca, sem o pai precisar saber nada sobre o comportamento responsivo do componente.
<!-- Este componente se adapta a QUALQUER contêiner onde é colocado -->
<article class="@container">
<div class="grid grid-cols-1 @md:grid-cols-[200px_1fr] gap-4">
<img
class="w-full @md:w-auto rounded-lg aspect-video @md:aspect-square object-cover"
src="/post-image.jpg"
alt=""
/>
<div>
<h2 class="text-lg @lg:text-xl font-semibold">Título do Post</h2>
<p class="mt-2 text-sm @md:text-base text-gray-600">
Trecho do post vai aqui...
</p>
<div class="mt-4 hidden @md:flex gap-2">
<span class="text-xs bg-gray-100 px-2 py-1 rounded">Tag</span>
</div>
</div>
</div>
</article>Novos Variants Que Realmente Importam#
A v4 adiciona vários novos variants que tenho usado constantemente. Eles preenchem lacunas reais.
O Variant starting:#
Ele mapeia para o CSS @starting-style, que permite definir o estado inicial de um elemento quando ele aparece pela primeira vez. Esta é a peça que faltava para animar a entrada de elementos sem JavaScript:
<dialog class="opacity-0 starting:opacity-0 open:opacity-100 transition-opacity duration-300">
<p>Este diálogo aparece com fade-in ao abrir</p>
</dialog>O variant starting: gera CSS dentro de um bloco @starting-style:
/* o que o Tailwind gera */
@starting-style {
dialog[open] {
opacity: 0;
}
}
dialog[open] {
opacity: 1;
transition: opacity 300ms;
}Isso é enorme para diálogos, popovers, menus dropdown — qualquer coisa que precise de uma animação de entrada. Antes disso, você precisava de JavaScript para adicionar uma classe no próximo frame, ou usava @keyframes. Agora é uma classe utilitária.
O Variant not-*#
Negação. Algo que queríamos há muito tempo:
<!-- todo filho exceto o último recebe uma borda -->
<div class="divide-y">
<div class="not-last:pb-4">Item 1</div>
<div class="not-last:pb-4">Item 2</div>
<div class="not-last:pb-4">Item 3</div>
</div>
<!-- estiliza tudo que não está desabilitado -->
<input class="not-disabled:hover:border-brand-500" />
<!-- nega atributos data -->
<div class="not-data-active:opacity-50">...</div>Os Variants nth-*#
Acesso direto a nth-child e nth-of-type:
<ul>
<li class="nth-1:font-bold">Primeiro item — negrito</li>
<li class="nth-even:bg-gray-50">Linhas pares — fundo cinza</li>
<li class="nth-odd:bg-white">Linhas ímpares — fundo branco</li>
<li class="nth-[3n+1]:text-brand-500">A cada terceiro+1 — cor da marca</li>
</ul>A sintaxe de colchetes (nth-[3n+1]) suporta qualquer expressão nth-child válida. Isso substitui muito CSS customizado que eu costumava escrever para striping de tabelas e padrões de grid.
O Variant in-* (Estado do Pai)#
Este é o inverso do group-*. Em vez de "quando meu pai (group) está em hover, estilize-me," é "quando estou dentro de um pai que corresponde a este estado, estilize-me":
<div class="in-data-active:bg-brand-50">
Isso ganha um fundo quando qualquer ancestral tem data-active
</div>O Variant Profundo Universal **:#
Estilize todos os descendentes, não apenas filhos diretos. Isso é poder controlado — use com moderação, mas é inestimável para conteúdo prose e saída de CMS:
<!-- todos os parágrafos dentro desta div, em qualquer profundidade -->
<div class="**:data-highlight:bg-yellow-100">
<section>
<p data-highlight>Isso fica destacado</p>
<div>
<p data-highlight>Isso também, aninhado mais fundo</p>
</div>
</section>
</div>Breaking Changes: O Que Realmente Quebrou#
Vou ser direto. Se você tem um projeto v3 grande, a migração não é trivial. Veja o que quebrou nos meus projetos:
1. Formato de Configuração#
Seu tailwind.config.js não funciona de cara. Você precisa:
- Convertê-lo para CSS
@theme(recomendado para a nova arquitetura) - Usar a camada de compatibilidade com a diretiva
@config(caminho rápido de migração)
/* migração rápida — mantenha seu config antigo */
@import "tailwindcss";
@config "../../tailwind.config.js";Essa ponte @config funciona, mas é explicitamente uma ferramenta de migração. A recomendação é migrar para @theme ao longo do tempo.
2. Utilities Depreciados Removidos#
Alguns utilities que estavam depreciados na v3 sumiram:
/* REMOVIDOS na v4 */
bg-opacity-* → use bg-black/50 (sintaxe de opacidade com barra)
text-opacity-* → use text-black/50
border-opacity-* → use border-black/50
flex-shrink-* → use shrink-*
flex-grow-* → use grow-*
overflow-ellipsis → use text-ellipsis
decoration-slice → use box-decoration-slice
decoration-clone → use box-decoration-clone
Se você já estava usando a sintaxe moderna na v3 (opacidade com barra, shrink-*), você está bem. Se não, são mudanças diretas de find-and-replace.
3. Mudanças na Paleta de Cores Padrão#
A paleta de cores padrão mudou levemente. Se você depende de valores exatos de cores da v3 (não pelo nome mas pelo valor hex real), pode notar diferenças visuais. As cores nomeadas (blue-500, gray-200) ainda existem mas alguns valores hex mudaram.
4. Detecção de Conteúdo#
A v3 exigia configuração explícita de content:
// v3
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
};A v4 usa detecção automática de conteúdo. Ela escaneia a raiz do seu projeto e encontra arquivos de template automaticamente. Na maioria das vezes "simplesmente funciona," mas se você tem uma estrutura de projeto incomum (monorepo com pacotes fora da raiz do projeto, arquivos de template em locais inesperados), pode precisar configurar os caminhos de fonte explicitamente:
@import "tailwindcss";
@source "../shared-components/**/*.tsx";
@source "../design-system/src/**/*.tsx";5. Mudanças na API de Plugins#
Se você escreveu plugins customizados, a API mudou. As funções addUtilities, addComponents, addBase e addVariant ainda funcionam pela camada de compatibilidade, mas a abordagem idiomática da v4 é CSS-nativa:
// plugin v3
plugin(function ({ addUtilities, theme }) {
addUtilities({
".scrollbar-hide": {
"-ms-overflow-style": "none",
"scrollbar-width": "none",
"&::-webkit-scrollbar": {
display: "none",
},
},
});
});/* v4 — apenas CSS */
@utility scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}A maioria dos plugins first-party (@tailwindcss/typography, @tailwindcss/forms, @tailwindcss/aspect-ratio) tem versões compatíveis com v4. Plugins de terceiros variam — verifique o repositório antes de migrar.
6. JIT É o Único Modo#
Na v3, você podia desativar o modo JIT (embora quase ninguém fizesse). Na v4, não existe modo não-JIT. Tudo é gerado sob demanda, sempre. Se você tinha algum motivo para usar o antigo engine AOT (ahead-of-time), esse caminho acabou.
7. Algumas Mudanças na Sintaxe de Variants#
Alguns variants foram renomeados ou tiveram comportamento alterado:
<!-- v3 -->
<div class="[&>*]:p-4">...</div>
<!-- v4 — a parte >* agora usa a sintaxe de variant inset -->
<div class="*:p-4">...</div>A sintaxe de variant arbitrário [&...] ainda funciona, mas a v4 fornece alternativas nomeadas para padrões comuns.
Guia de Migração: O Processo Real#
Veja como eu realmente migrei, não o caminho feliz da documentação mas como o processo realmente foi.
Passo 1: Execute o Codemod Oficial#
O Tailwind fornece um codemod que lida com a maioria das mudanças mecânicas:
npx @tailwindcss/upgradeEle faz muita coisa automaticamente:
- Converte diretivas
@tailwindpara@import "tailwindcss" - Renomeia classes utilitárias depreciadas
- Atualiza sintaxe de variants
- Converte utilities de opacidade para sintaxe de barra (
bg-opacity-50parabg-black/50) - Cria um bloco
@themebásico a partir do seu config
O Que o Codemod Faz Bem#
- Renomeação de classes utilitárias (quase perfeito)
- Mudanças de sintaxe de diretivas
- Valores de tema simples (cores, espaçamento, fontes)
- Migração de sintaxe de opacidade
O Que o Codemod NÃO Faz#
- Conversões complexas de plugins
- Valores de config dinâmicos (chamadas
theme()em JavaScript) - Configuração condicional de tema (ex: valores de tema baseados em ambiente)
- Migrações de API de plugins customizados
- Casos extremos de valores arbitrários onde o novo parser interpreta diferente
- Nomes de classe construídos dinamicamente em JavaScript (template literals, concatenação de strings)
Passo 2: Corrija a Configuração do PostCSS#
Para a maioria dos setups, você vai atualizar seu config PostCSS:
// postcss.config.js — v4
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
};Nota: o nome do plugin mudou de tailwindcss para @tailwindcss/postcss. Se você usa Vite, pode pular o PostCSS completamente e usar o plugin Vite:
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});Passo 3: Converta a Configuração de Tema#
Esta é a parte manual. Pegue os valores de tema do seu tailwind.config.js e converta-os para @theme:
// config v3 — antes
module.exports = {
theme: {
extend: {
colors: {
brand: {
light: "#60a5fa",
DEFAULT: "#3b82f6",
dark: "#1d4ed8",
},
},
fontSize: {
"2xs": ["0.65rem", { lineHeight: "1rem" }],
},
animation: {
"fade-in": "fade-in 0.5s ease-out",
},
keyframes: {
"fade-in": {
"0%": { opacity: "0" },
"100%": { opacity: "1" },
},
},
},
},
};/* CSS v4 — depois */
@import "tailwindcss";
@theme {
--color-brand-light: #60a5fa;
--color-brand: #3b82f6;
--color-brand-dark: #1d4ed8;
--text-2xs: 0.65rem;
--text-2xs--line-height: 1rem;
--animate-fade-in: fade-in 0.5s ease-out;
}
@keyframes fade-in {
0% { opacity: 0; }
100% { opacity: 1; }
}Note que keyframes saem do @theme e se tornam @keyframes CSS regulares. O nome da animação no @theme apenas os referencia. Isso é mais limpo — keyframes são CSS, devem ser escritos como CSS.
Passo 4: Testes de Regressão Visual#
Isso é inegociável. Após a migração, abri cada página do meu app e verifiquei visualmente. Também rodei meus testes de screenshot do Playwright (se você os tiver). O codemod é bom mas não perfeito. Coisas que peguei na revisão visual:
- Alguns lugares onde a migração de sintaxe de opacidade produziu resultados levemente diferentes
- Saída de plugin customizado que não migrou
- Mudanças de empilhamento de z-index devido à ordenação de layers
- Alguns overrides
!importantque se comportaram diferente com cascade layers
Passo 5: Atualize Dependências de Terceiros#
Verifique cada pacote relacionado ao Tailwind:
{
"@tailwindcss/typography": "^1.0.0",
"@tailwindcss/forms": "^1.0.0",
"@tailwindcss/container-queries": "REMOVA — agora é nativo",
"tailwindcss-animate": "verifique suporte v4",
"prettier-plugin-tailwindcss": "atualize para a última versão"
}O plugin @tailwindcss/container-queries não é mais necessário — container queries são nativos. Outros plugins precisam de suas versões compatíveis com v4.
Trabalhando com Next.js#
Como uso Next.js para a maioria dos projetos, aqui está o setup específico.
Abordagem PostCSS (Recomendada para Next.js)#
O Next.js usa PostCSS por baixo dos panos, então o plugin PostCSS é o encaixe natural:
npm install tailwindcss @tailwindcss/postcss// postcss.config.mjs
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};/* app/globals.css */
@import "tailwindcss";
@theme {
--font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif;
--font-mono: "JetBrains Mono Variable", ui-monospace, monospace;
}Esse é o setup completo. Sem tailwind.config.js, sem autoprefixer (a v4 lida com vendor prefixes internamente).
Ordem de Import CSS#
Uma coisa que me confundiu: a ordem de import CSS importa mais na v4 por causa dos cascade layers. Seu @import "tailwindcss" deve vir antes dos seus estilos customizados:
/* ordem correta */
@import "tailwindcss";
@import "./theme.css";
@import "./custom-utilities.css";
/* seus @theme, @utility, etc. inline */Se você importar CSS customizado antes do Tailwind, seus estilos podem acabar em um cascade layer de menor prioridade e ser sobrescritos inesperadamente.
Modo Escuro#
O modo escuro funciona da mesma forma conceitualmente mas a configuração migrou para CSS:
@import "tailwindcss";
/* Use modo escuro baseado em classe (padrão é baseado em media) */
@variant dark (&:where(.dark, .dark *));Isso substitui o config da v3:
// v3
module.exports = {
darkMode: "class",
};A abordagem @variant é mais flexível. Você pode definir modo escuro como quiser — baseado em classe, baseado em data-attribute ou baseado em media-query:
/* abordagem de data attribute */
@variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
/* media query — este é o padrão, então você não precisa declarar */
@variant dark (@media (prefers-color-scheme: dark));Compatibilidade com Turbopack#
Se você usa Next.js com Turbopack (que agora é o bundler de dev padrão), a v4 funciona muito bem. O engine Rust combina bem com a própria arquitetura baseada em Rust do Turbopack. Medi os tempos de inicialização do dev:
| Setup | v3 + Webpack | v3 + Turbopack | v4 + Turbopack |
|---|---|---|---|
| Cold start | 4,8s | 2,1s | 1,3s |
| HMR (mudança CSS) | 450ms | 180ms | 40ms |
Os 40ms de HMR para mudanças CSS são mal perceptíveis. Parece instantâneo.
Mergulho em Performance: Além da Velocidade de Build#
Os benefícios do engine Oxide vão além da velocidade bruta de build.
Uso de Memória#
A v4 usa significativamente menos memória. No meu projeto de 847 componentes:
| Métrica | v3 | v4 |
|---|---|---|
| Pico de memória (build) | 380MB | 45MB |
| Estado estável (dev) | 210MB | 28MB |
Isso importa para pipelines CI/CD onde memória é limitada e para máquinas de desenvolvimento rodando dez processos simultaneamente.
Tamanho do CSS de Saída#
A v4 gera CSS de saída ligeiramente menor porque o novo engine é melhor em deduplicação e eliminação de código morto:
Saída v3: 34,2 KB (gzipped)
Saída v4: 29,8 KB (gzipped)
Uma redução de 13% sem mudar nenhum código. Não é transformador, mas é performance grátis.
Tree Shaking de Valores de Tema#
Na v4, se você define um valor de tema mas nunca o usa nos seus templates, a custom property CSS correspondente ainda é emitida (está no @theme, que mapeia para variáveis :root). No entanto, as classes utilitárias para valores não usados não são geradas. Isso é o mesmo comportamento JIT da v3 mas vale notar: suas custom properties CSS estão sempre disponíveis, mesmo para valores sem uso de utilitários.
Se você quer impedir que certos valores de tema gerem custom properties CSS, pode usar @theme inline:
@theme inline {
/* Esses valores geram utilities mas NÃO custom properties CSS */
--color-internal-debug: #ff00ff;
--spacing-magic-number: 3.7rem;
}Isso é útil para design tokens internos que você não quer expostos como variáveis CSS.
Avançado: Compondo Temas para Multi-Marca#
Um padrão que a v4 facilita significativamente é o tema multi-marca. Como os valores de tema são custom properties CSS, você pode trocá-los em runtime:
@import "tailwindcss";
@theme {
--color-brand: var(--brand-primary, #3b82f6);
--color-brand-light: var(--brand-light, #60a5fa);
--color-brand-dark: var(--brand-dark, #1d4ed8);
}
/* Overrides de marca */
.theme-acme {
--brand-primary: #e11d48;
--brand-light: #fb7185;
--brand-dark: #9f1239;
}
.theme-globex {
--brand-primary: #059669;
--brand-light: #34d399;
--brand-dark: #047857;
}<body class="theme-acme">
<!-- todos os bg-brand, text-brand, etc. usam cores da Acme -->
<div class="bg-brand text-white">Acme Corp</div>
</body>Na v3, isso exigia um plugin customizado ou configuração complexa de variáveis CSS fora do Tailwind. Na v4, é natural — o tema são variáveis CSS, e variáveis CSS cascateiam. Esse é o tipo de coisa que faz a abordagem CSS-first parecer certa.
O Que Sinto Falta da v3#
Vou ser equilibrado. Há coisas que a v3 fazia que genuinamente sinto falta na v4:
1. Config JavaScript para temas programáticos. Eu tinha um projeto onde gerávamos escalas de cores a partir de uma única cor de marca usando uma função JavaScript no config. Na v4, você não pode fazer isso no @theme — precisaria de um passo de build que gera o arquivo CSS, ou computar as cores uma vez e colar. A camada de compatibilidade @config ajuda, mas não é a história de longo prazo.
2. IntelliSense era melhor no lançamento. A extensão VS Code da v3 teve anos de polimento. O IntelliSense da v4 funciona mas tinha algumas lacunas no início — valores customizados de @theme às vezes não autocompletavam, e definições de @utility nem sempre eram detectadas. Isso melhorou substancialmente com atualizações recentes, mas vale mencionar.
3. Maturidade do ecossistema. O ecossistema em torno da v3 era enorme. Headless UI, Radix, shadcn/ui, Flowbite, DaisyUI — tudo foi testado contra a v3. O suporte a v4 está sendo lançado mas não é universal. Tive que submeter um PR para uma biblioteca de componentes para corrigir compatibilidade com v4.
Você Deveria Migrar?#
Aqui está meu framework de decisão depois de viver com a v4 por várias semanas:
Migre Agora Se:#
- Você está começando um novo projeto (escolha óbvia — comece com v4)
- Seu projeto tem plugins customizados mínimos
- Você quer os benefícios de performance para projetos grandes
- Você já está usando padrões modernos do Tailwind (opacidade com barra,
shrink-*, etc.) - Você precisa de container queries e prefere não adicionar um plugin
Espere Se:#
- Você depende fortemente de plugins Tailwind de terceiros que não suportam v4 ainda
- Você tem configuração de tema programática complexa
- Seu projeto é estável e não está em desenvolvimento ativo (por que mexer?)
- Você está no meio de um sprint de features (migre entre sprints, não durante)
Não Migre Se:#
- Você está na v2 ou anterior (atualize para v3 primeiro, estabilize, depois considere v4)
- Seu projeto termina nos próximos meses (não vale o transtorno)
Minha Opinião Honesta#
Para projetos novos, v4 é a escolha óbvia. A configuração CSS-first é mais limpa, o engine é dramaticamente mais rápido, e as novas features (container queries, @starting-style, novos variants) são genuinamente úteis.
Para projetos existentes, recomendo uma abordagem gradual:
- Agora: Comece qualquer projeto novo na v4
- Em breve: Experimente convertendo um pequeno projeto interno para v4
- Quando pronto: Migre projetos de produção durante um sprint tranquilo, com testes de regressão visual
A migração não é dolorosa se você se preparar para ela. O codemod lida com 80% do trabalho. Os 20% restantes são manuais mas diretos. Reserve um dia para um projeto médio, dois a três dias para um grande.
O Tailwind v4 é o que o Tailwind deveria ter sido desde o início. A configuração JavaScript sempre foi uma concessão às ferramentas de sua época. Configuração CSS-first, cascade layers nativos, um engine Rust — estas não são tendências, é o framework alcançando a plataforma. A plataforma web melhorou, e o Tailwind v4 abraça isso ao invés de lutar contra.
O movimento de escrever seus design tokens em CSS, compô-los com funcionalidades CSS e deixar o próprio cascade do navegador lidar com especificidade — essa é a direção certa. Levou quatro versões major para chegar aqui, mas o resultado é a versão mais coerente do Tailwind até agora.
Comece seu próximo projeto com ele. Você não vai olhar para trás.