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 Tailwind CSS v4 são três coisas ao mesmo tempo:
tailwind.config.js como padrão@layer nativo, container queries, @starting-style e cascade layers são cidadãos de primeira classeA 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.
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."
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.
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 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.
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.
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.
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.
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>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 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.
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) |
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>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>A v4 adiciona vários novos variants que tenho usado constantemente. Eles preenchem lacunas reais.
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.
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>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.
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>**:#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>Vou ser direto. Se você tem um projeto v3 grande, a migração não é trivial. Veja o que quebrou nos meus projetos:
Seu tailwind.config.js não funciona de cara. Você precisa:
@theme (recomendado para a nova arquitetura)@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.
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.
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.
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";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.
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.
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.
Veja como eu realmente migrei, não o caminho feliz da documentação mas como o processo realmente foi.
O Tailwind fornece um codemod que lida com a maioria das mudanças mecânicas:
npx @tailwindcss/upgradeEle faz muita coisa automaticamente:
@tailwind para @import "tailwindcss"bg-opacity-50 para bg-black/50)@theme básico a partir do seu configtheme() em JavaScript)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()],
});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.
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:
!important que se comportaram diferente com cascade layersVerifique 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.
Como uso Next.js para a maioria dos projetos, aqui está o setup específico.
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).
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.
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));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.
Os benefícios do engine Oxide vão além da velocidade bruta de build.
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.
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.
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.
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.
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.
Aqui está meu framework de decisão depois de viver com a v4 por várias semanas:
shrink-*, etc.)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:
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.