Tailwind CSS v4: Cosa È Davvero Cambiato e Se Conviene Migrare
Configurazione CSS-first, integrazione @layer, container query integrate, prestazioni del nuovo motore, breaking change e la mia esperienza onesta di migrazione da v3 a v4.
Uso Tailwind CSS dalla v1.x, quando metà della community pensava che fosse un abominio e l'altra metà non riusciva a smettere di spedire prodotti con esso. Ogni versione major è stata un salto significativo, ma la v4 è diversa. Non è solo un rilascio di funzionalità. È una riscrittura architetturale dalle fondamenta che cambia il contratto fondamentale tra te e il framework.
Dopo aver migrato due progetti in produzione dalla v3 alla v4 e aver avviato tre nuovi progetti sulla v4 da zero, ho un quadro chiaro di cosa è genuinamente migliore, cosa è ancora grezzo, e se dovresti migrare oggi. Niente hype, niente indignazione — solo quello che ho osservato.
Il Quadro Generale: Cos'è la v4#
Tailwind CSS v4 è tre cose contemporaneamente:
- Un nuovo motore — riscritto da JavaScript a Rust (il motore Oxide), rendendo le build drammaticamente più veloci
- Un nuovo paradigma di configurazione — la configurazione CSS-first sostituisce
tailwind.config.jscome default - Un'integrazione più stretta con la piattaforma CSS —
@layernativo, container query,@starting-stylee i cascade layer sono cittadini di prima classe
Il titolo che vedrai ovunque è "10x più veloce." È reale, ma sottovaluta il cambiamento effettivo. Il modello mentale per configurare ed estendere Tailwind è fondamentalmente cambiato. Ora lavori con CSS, non con un oggetto di configurazione JavaScript che genera CSS.
Ecco come appare un setup minimo di Tailwind v4:
/* app.css — questo è l'intero setup */
@import "tailwindcss";Tutto qui. Nessun file di configurazione. Nessuna configurazione del plugin PostCSS (per la maggior parte dei setup). Niente direttive @tailwind base; @tailwind components; @tailwind utilities;. Un import e sei operativo.
Confrontalo con la 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: {},
},
};Tre file ridotti a una riga. Non è solo meno boilerplate — è meno superficie per configurazioni errate. Nella v4, il rilevamento dei contenuti è automatico. Scansiona i file del tuo progetto senza bisogno di specificare pattern glob.
Configurazione CSS-First con @theme#
Questo è il più grande cambiamento concettuale. Nella v3, personalizzavi Tailwind attraverso un oggetto di configurazione 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",
},
},
},
};Nella v4, tutto questo vive nel CSS usando la direttiva @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;
}All'inizio ho resistito a questo approccio. Mi piaceva avere un singolo oggetto JavaScript dove potevo vedere l'intero design system. Ma dopo una settimana con l'approccio CSS, ho cambiato idea per tre ragioni:
1. Le custom property CSS native vengono esposte automaticamente. Ogni valore che definisci in @theme diventa una custom property CSS su :root. Questo significa che i valori del tuo tema sono accessibili nel CSS puro, nei CSS Modules, nei tag <style>, ovunque il CSS funzioni:
/* ottieni questo gratuitamente */
:root {
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}/* usali ovunque — non serve Tailwind */
.custom-element {
border: 2px solid var(--color-brand-500);
}2. Puoi usare funzionalità CSS dentro @theme. Media query, light-dark(), calc() — il vero CSS funziona qui perché è vero CSS:
@theme {
--color-surface: light-dark(#ffffff, #0a0a0a);
--color-text: light-dark(#0a0a0a, #fafafa);
--spacing-container: calc(100vw - 2rem);
}3. Co-locazione con il tuo altro CSS. Il tuo tema, le tue utility personalizzate e i tuoi stili base vivono tutti nello stesso linguaggio, nello stesso file se vuoi. Non c'è cambio di contesto tra "mondo CSS" e "mondo di configurazione JavaScript."
Override vs Estensione del Tema Predefinito#
Nella v3 avevi theme (sostituzione) vs theme.extend (merge). Nella v4, il modello mentale è diverso:
@import "tailwindcss";
/* Questo ESTENDE il tema predefinito — aggiunge colori brand insieme a quelli esistenti */
@theme {
--color-brand-500: #3b82f6;
}Se vuoi sostituire completamente un namespace (come rimuovere tutti i colori predefiniti), usi @theme con il reset wildcard --color-*:
@import "tailwindcss";
@theme {
/* Cancella prima tutti i colori predefiniti */
--color-*: initial;
/* Ora definisci solo i tuoi colori */
--color-white: #ffffff;
--color-black: #000000;
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}Questo pattern di reset wildcard è elegante. Scegli esattamente quali parti del tema predefinito mantenere e quali sostituire. Vuoi tutti gli spacing predefiniti ma colori personalizzati? Resetta --color-*: initial; e lascia lo spacing com'è.
File di Tema Multipli#
Per progetti più grandi, puoi dividere il tuo tema tra file:
/* 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";Questo è molto più pulito del pattern v3 di avere un gigantesco tailwind.config.js o cercare di dividerlo con require().
Il Motore Oxide: È Davvero 10x Più Veloce#
Il motore della v4 di Tailwind è una riscrittura completa in Rust. Lo chiamano Oxide. Ero scettico sulla dichiarazione "10x più veloce" — i numeri marketing raramente sopravvivono al contatto con progetti reali. Quindi ho fatto un benchmark.
Il mio progetto di test: Un'app Next.js con 847 componenti, 142 pagine, circa 23.000 utilizzi di classi Tailwind.
| Metrica | v3 (Node) | v4 (Oxide) | Miglioramento |
|---|---|---|---|
| Build iniziale | 4.280ms | 387ms | 11x |
| Incrementale (modifica 1 file) | 340ms | 18ms | 19x |
| Rebuild completa (clean) | 5.100ms | 510ms | 10x |
| Avvio dev server | 3.200ms | 290ms | 11x |
La dichiarazione "10x" è conservativa per il mio progetto. Le build incrementali sono dove brilla davvero — 18ms significa che è essenzialmente istantaneo. Salvi un file e il browser ha i nuovi stili prima che tu possa cambiare tab.
Perché È Così Più Veloce?#
Tre ragioni:
1. Rust invece di JavaScript. Il parser CSS core, il rilevamento delle classi e la generazione del codice sono tutti Rust nativo. Questa non è una situazione "riscriviamo in Rust per divertimento" — il parsing CSS è genuinamente un lavoro CPU-bound dove il codice nativo ha un enorme vantaggio rispetto a V8.
2. Niente PostCSS nel percorso critico. Nella v3, Tailwind era un plugin PostCSS. Ogni build significava: parse CSS in AST PostCSS, esegui plugin Tailwind, serializza di nuovo in stringa CSS, poi gli altri plugin PostCSS eseguono. Nella v4, Tailwind ha il suo parser CSS che va direttamente da sorgente a output. PostCSS è ancora supportato per compatibilità, ma il percorso principale lo salta del tutto.
3. Elaborazione incrementale più intelligente. Il nuovo motore fa caching aggressivo. Quando modifichi un singolo file, ri-scansiona solo quel file per i nomi delle classi e rigenera solo le regole CSS che sono cambiate. Il motore v3 era più intelligente di quanto la gente gli riconosca (la modalità JIT era già incrementale), ma la v4 va molto oltre con il tracciamento fine delle dipendenze.
La Velocità Conta Davvero?#
Sì, ma non per la ragione che penseresti. Per la maggior parte dei progetti, la velocità di build della v3 era "ok." Aspettavi qualche centinaio di millisecondi in dev. Non doloroso.
La velocità della v4 conta perché rende Tailwind invisibile nella tua toolchain. Quando le build sono sotto i 20ms, smetti di pensare a Tailwind come a un passaggio di build. Diventa come la syntax highlighting — sempre presente, mai d'intralcio. Quella differenza psicologica è significativa nell'arco di un'intera giornata di sviluppo.
Integrazione Nativa @layer#
Nella v3, Tailwind usava il suo sistema di layer con @layer base, @layer components e @layer utilities. Sembravano cascade layer CSS ma non lo erano — erano direttive specifiche di Tailwind che controllavano dove il CSS generato appariva nell'output.
Nella v4, Tailwind usa veri cascade layer CSS:
/* output v4 — semplificato */
@layer theme, base, components, utilities;
@layer base {
/* reset, preflight */
}
@layer components {
/* le tue classi componente */
}
@layer utilities {
/* tutte le classi utility generate */
}Questo è un cambiamento significativo perché i cascade layer CSS hanno implicazioni reali sulla specificità. Una regola in un layer a priorità inferiore perde sempre contro una regola in un layer a priorità superiore, indipendentemente dalla specificità del selettore. Questo significa:
@layer components {
/* specificità: 0-1-0 */
.card { padding: 1rem; }
}
@layer utilities {
/* specificità: 0-1-0 — stessa specificità ma vince perché il layer utilities è dopo */
.p-4 { padding: 1rem; }
}Le utility sovrascrivono sempre i componenti. I componenti sovrascrivono sempre base. Questo è come Tailwind funzionava concettualmente nella v3, ma ora è imposto dal meccanismo dei cascade layer del browser, non dalla manipolazione dell'ordine nel sorgente.
Aggiungere Utility Personalizzate#
Nella v3, definivi utility personalizzate con una plugin API o @layer utilities:
// v3 — approccio plugin
const plugin = require("tailwindcss/plugin");
module.exports = {
plugins: [
plugin(function ({ addUtilities }) {
addUtilities({
".text-balance": {
"text-wrap": "balance",
},
".text-pretty": {
"text-wrap": "pretty",
},
});
}),
],
};Nella v4, le utility personalizzate si definiscono con la direttiva @utility:
@import "tailwindcss";
@utility text-balance {
text-wrap: balance;
}
@utility text-pretty {
text-wrap: pretty;
}La direttiva @utility dice a Tailwind "questa è una classe utility — mettila nel layer utilities e permetti che sia usata con le variant." Quest'ultima parte è fondamentale. Una utility definita con @utility funziona automaticamente con hover:, focus:, md: e ogni altra variant:
<p class="text-pretty md:text-balance">...</p>Variant Personalizzate#
Puoi anche definire variant personalizzate con @variant:
@import "tailwindcss";
@variant hocus (&:hover, &:focus);
@variant theme-dark (.dark &);<button class="hocus:bg-brand-500 theme-dark:text-white">
Cliccami
</button>Questo sostituisce l'API plugin addVariant della v3 per la maggior parte dei casi d'uso. È meno potente (non puoi fare generazione programmatica di variant), ma copre il 90% di quello che le persone fanno realmente.
Container Query: Integrate, Nessun Plugin#
Le container query erano una delle funzionalità più richieste nella v3. Potevi ottenerle con il plugin @tailwindcss/container-queries, ma era un'aggiunta. Nella v4, sono integrate nel framework.
Utilizzo Base#
Segna un container con @container e interroga la sua dimensione con il prefisso @:
<!-- segna il parent come container -->
<div class="@container">
<!-- responsive alla larghezza del parent, non del 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>Le variant @md, @lg, ecc. funzionano come breakpoint responsive ma sono relative al più vicino antenato @container invece del viewport. I valori dei breakpoint corrispondono ai breakpoint predefiniti di Tailwind:
| Variant | Min-width |
|---|---|
@sm | 24rem (384px) |
@md | 28rem (448px) |
@lg | 32rem (512px) |
@xl | 36rem (576px) |
@2xl | 42rem (672px) |
Container con Nome#
Puoi dare un nome ai container per interrogare antenati specifici:
<div class="@container/sidebar">
<div class="@container/card">
<!-- interroga il container card -->
<div class="@md/card:text-lg">...</div>
<!-- interroga il container sidebar -->
<div class="@lg/sidebar:hidden">...</div>
</div>
</div>Perché Questo Conta#
Le container query cambiano il modo in cui pensi al design responsive. Invece di "a questa larghezza del viewport, mostra tre colonne," dici "quando il container di questo componente è abbastanza largo, mostra tre colonne." I componenti diventano veramente autonomi. Puoi spostare un componente card da un layout a larghezza piena a una sidebar e si adatta automaticamente. Nessuna ginnastica con le media query.
Ho iniziato a fare refactoring delle mie librerie di componenti per usare container query di default invece di breakpoint del viewport. Il risultato sono componenti che funzionano ovunque li posizioni, senza che il parent debba sapere nulla del comportamento responsive del componente.
<!-- Questo componente si adatta a QUALSIASI container in cui è posizionato -->
<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">Titolo del Post</h2>
<p class="mt-2 text-sm @md:text-base text-gray-600">
L'estratto del post va qui...
</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>Nuove Variant che Contano Davvero#
La v4 aggiunge diverse nuove variant che ho usato costantemente. Riempiono lacune reali.
La Variant starting:#
Questa corrisponde a CSS @starting-style, che ti permette di definire lo stato iniziale di un elemento quando appare per la prima volta. Questo è il pezzo mancante per animare l'ingresso degli elementi senza JavaScript:
<dialog class="opacity-0 starting:opacity-0 open:opacity-100 transition-opacity duration-300">
<p>Questa dialog appare con fade in quando viene aperta</p>
</dialog>La variant starting: genera CSS dentro un blocco @starting-style:
/* cosa genera Tailwind */
@starting-style {
dialog[open] {
opacity: 0;
}
}
dialog[open] {
opacity: 1;
transition: opacity 300ms;
}Questo è enorme per dialog, popover, menu dropdown — qualsiasi cosa che necessiti un'animazione di ingresso. Prima di questo, serviva JavaScript per aggiungere una classe al frame successivo, o usavi @keyframes. Ora è una classe utility.
La Variant not-*#
Negazione. Qualcosa che abbiamo voluto da sempre:
<!-- ogni figlio tranne l'ultimo ha un bordo -->
<div class="divide-y">
<div class="not-last:pb-4">Elemento 1</div>
<div class="not-last:pb-4">Elemento 2</div>
<div class="not-last:pb-4">Elemento 3</div>
</div>
<!-- stile per tutto ciò che non è disabilitato -->
<input class="not-disabled:hover:border-brand-500" />
<!-- nega attributi data -->
<div class="not-data-active:opacity-50">...</div>Le Variant nth-*#
Accesso diretto a nth-child e nth-of-type:
<ul>
<li class="nth-1:font-bold">Primo elemento — grassetto</li>
<li class="nth-even:bg-gray-50">Righe pari — sfondo grigio</li>
<li class="nth-odd:bg-white">Righe dispari — sfondo bianco</li>
<li class="nth-[3n+1]:text-brand-500">Ogni terzo+1 — colore brand</li>
</ul>La sintassi con parentesi (nth-[3n+1]) supporta qualsiasi espressione nth-child valida. Questo sostituisce molto CSS personalizzato che scrivevo per striping delle tabelle e pattern nelle grid.
La Variant in-* (Stato del Parent)#
Questa è l'inverso di group-*. Invece di "quando il mio parent (group) è in hover, stilizza me," è "quando sono dentro un parent che corrisponde a questo stato, stilizza me":
<div class="in-data-active:bg-brand-50">
Questo ottiene uno sfondo quando qualsiasi antenato ha data-active
</div>La Variant **: Deep Universal#
Stilizza tutti i discendenti, non solo i figli diretti. Questo è potere controllato — usalo con parsimonia, ma è inestimabile per contenuti prose e output CMS:
<!-- tutti i paragrafi dentro questo div, a qualsiasi profondità -->
<div class="**:data-highlight:bg-yellow-100">
<section>
<p data-highlight>Questo viene evidenziato</p>
<div>
<p data-highlight>Anche questo, annidato più in profondità</p>
</div>
</section>
</div>Breaking Change: Cosa Si È Rotto Davvero#
Sarò diretto. Se hai un grande progetto v3, la migrazione non è banale. Ecco cosa si è rotto nei miei progetti:
1. Formato di Configurazione#
Il tuo tailwind.config.js non funziona subito. Devi:
- Convertirlo in CSS
@theme(consigliato per la nuova architettura) - Usare il layer di compatibilità con la direttiva
@config(percorso di migrazione rapido)
/* migrazione rapida — mantieni la tua vecchia config */
@import "tailwindcss";
@config "../../tailwind.config.js";Questo ponte @config funziona, ma è esplicitamente uno strumento di migrazione. La raccomandazione è di passare a @theme nel tempo.
2. Utility Deprecate Rimosse#
Alcune utility che erano deprecate nella v3 sono scomparse:
/* RIMOSSE nella v4 */
bg-opacity-* → usa bg-black/50 (sintassi opacità con slash)
text-opacity-* → usa text-black/50
border-opacity-* → usa border-black/50
flex-shrink-* → usa shrink-*
flex-grow-* → usa grow-*
overflow-ellipsis → usa text-ellipsis
decoration-slice → usa box-decoration-slice
decoration-clone → usa box-decoration-clone
Se stavi già usando la sintassi moderna nella v3 (opacità con slash, shrink-*), sei a posto. In caso contrario, sono modifiche semplici di trova-e-sostituisci.
3. Modifiche alla Palette Colori Predefinita#
La palette colori predefinita è cambiata leggermente. Se dipendi dai valori esatti dei colori della v3 (non per nome ma per il valore hex effettivo), potresti notare differenze visive. I colori con nome (blue-500, gray-200) esistono ancora ma alcuni valori hex sono cambiati.
4. Rilevamento dei Contenuti#
La v3 richiedeva una configurazione esplicita content:
// v3
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
};La v4 usa il rilevamento automatico dei contenuti. Scansiona la root del tuo progetto e trova automaticamente i file template. Questo "funziona e basta" nella maggior parte dei casi, ma se hai una struttura di progetto insolita (monorepo con pacchetti fuori dalla root del progetto, file template in posizioni inaspettate), potresti dover configurare esplicitamente i percorsi dei sorgenti:
@import "tailwindcss";
@source "../shared-components/**/*.tsx";
@source "../design-system/src/**/*.tsx";5. Modifiche all'API dei Plugin#
Se hai scritto plugin personalizzati, l'API è cambiata. Le funzioni addUtilities, addComponents, addBase e addVariant funzionano ancora attraverso il layer di compatibilità, ma l'approccio idiomatico v4 è nativo CSS:
// plugin v3
plugin(function ({ addUtilities, theme }) {
addUtilities({
".scrollbar-hide": {
"-ms-overflow-style": "none",
"scrollbar-width": "none",
"&::-webkit-scrollbar": {
display: "none",
},
},
});
});/* v4 — solo CSS */
@utility scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}La maggior parte dei plugin first-party (@tailwindcss/typography, @tailwindcss/forms, @tailwindcss/aspect-ratio) ha versioni compatibili con la v4. I plugin di terze parti sono un terno al lotto — controlla il loro repo prima di migrare.
6. JIT È l'Unica Modalità#
Nella v3, potevi rinunciare alla modalità JIT (anche se quasi nessuno lo faceva). Nella v4, non esiste una modalità non-JIT. Tutto viene generato su richiesta, sempre. Se avevi qualche ragione per usare il vecchio motore AOT (ahead-of-time), quella strada non esiste più.
7. Alcune Modifiche alla Sintassi delle Variant#
Alcune variant sono state rinominate o hanno cambiato comportamento:
<!-- v3 -->
<div class="[&>*]:p-4">...</div>
<!-- v4 — la parte >* ora usa la sintassi variant inset -->
<div class="*:p-4">...</div>La sintassi delle variant arbitrarie [&...] funziona ancora, ma la v4 fornisce alternative con nome per i pattern comuni.
Guida alla Migrazione: Il Processo Reale#
Ecco come ho effettivamente migrato, non il percorso felice dalla documentazione ma come il processo è apparso realmente.
Passo 1: Esegui il Codemod Ufficiale#
Tailwind fornisce un codemod che gestisce la maggior parte delle modifiche meccaniche:
npx @tailwindcss/upgradeQuesto fa molto automaticamente:
- Converte le direttive
@tailwindin@import "tailwindcss" - Rinomina le classi utility deprecate
- Aggiorna la sintassi delle variant
- Converte le utility di opacità nella sintassi con slash (
bg-opacity-50inbg-black/50) - Crea un blocco
@themebase dalla tua config
Cosa il Codemod Gestisce Bene#
- Rinomina delle classi utility (quasi perfetto)
- Modifiche alla sintassi delle direttive
- Valori di tema semplici (colori, spacing, font)
- Migrazione della sintassi dell'opacità
Cosa il Codemod NON Gestisce#
- Conversioni complesse di plugin
- Valori di configurazione dinamici (chiamate
theme()in JavaScript) - Configurazione condizionale del tema (es. valori di tema basati sull'ambiente)
- Migrazioni dell'API dei plugin personalizzati
- Casi limite di valori arbitrari dove il nuovo parser interpreta diversamente
- Nomi di classi costruiti dinamicamente in JavaScript (template literal, concatenazione di stringhe)
Passo 2: Correggi la Configurazione PostCSS#
Per la maggior parte dei setup, aggiornerai la tua config PostCSS:
// postcss.config.js — v4
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
};Nota: il nome del plugin è cambiato da tailwindcss a @tailwindcss/postcss. Se usi Vite, puoi saltare PostCSS interamente e usare il plugin Vite:
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});Passo 3: Converti la Configurazione del Tema#
Questa è la parte manuale. Prendi i valori del tema dal tuo tailwind.config.js e convertili in @theme:
// config v3 — prima
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 — dopo */
@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; }
}Nota che i keyframes escono da @theme e diventano normali CSS @keyframes. Il nome dell'animazione in @theme li referenzia semplicemente. Questo è più pulito — i keyframes sono CSS, dovrebbero essere scritti come CSS.
Passo 4: Test di Regressione Visiva#
Questo non è negoziabile. Dopo la migrazione, ho aperto ogni pagina della mia app e l'ho controllata visivamente. Ho anche eseguito i miei test screenshot con Playwright (se li hai). Il codemod è buono ma non perfetto. Cose che ho individuato nella revisione visiva:
- Alcuni punti dove la migrazione della sintassi dell'opacità ha prodotto risultati leggermente diversi
- Output di plugin personalizzati che non sono stati trasferiti
- Modifiche allo stacking dello z-index dovute all'ordinamento dei layer
- Alcuni override
!importantche si sono comportati diversamente con i cascade layer
Passo 5: Aggiorna le Dipendenze di Terze Parti#
Controlla ogni pacchetto relativo a Tailwind:
{
"@tailwindcss/typography": "^1.0.0",
"@tailwindcss/forms": "^1.0.0",
"@tailwindcss/container-queries": "RIMUOVI — ora integrato",
"tailwindcss-animate": "verifica supporto v4",
"prettier-plugin-tailwindcss": "aggiorna all'ultimo"
}Il plugin @tailwindcss/container-queries non è più necessario — le container query sono integrate. Gli altri plugin necessitano delle loro versioni compatibili con la v4.
Lavorare con Next.js#
Dato che uso Next.js per la maggior parte dei progetti, ecco il setup specifico.
Approccio PostCSS (Consigliato per Next.js)#
Next.js usa PostCSS sotto il cofano, quindi il plugin PostCSS è la scelta naturale:
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;
}Questo è il setup completo. Nessun tailwind.config.js, nessun autoprefixer (la v4 gestisce i prefissi vendor internamente).
Ordine degli Import CSS#
Una cosa che mi ha fatto inciampare: l'ordine degli import CSS conta di più nella v4 a causa dei cascade layer. Il tuo @import "tailwindcss" dovrebbe venire prima dei tuoi stili personalizzati:
/* ordine corretto */
@import "tailwindcss";
@import "./theme.css";
@import "./custom-utilities.css";
/* i tuoi @theme, @utility inline, ecc. */Se importi CSS personalizzato prima di Tailwind, i tuoi stili potrebbero finire in un cascade layer a priorità inferiore e essere sovrascritti inaspettatamente.
Modalità Scura#
La modalità scura funziona allo stesso modo concettualmente ma la configurazione si è spostata nel CSS:
@import "tailwindcss";
/* Usa la modalità scura basata su classe (il default è basato su media) */
@variant dark (&:where(.dark, .dark *));Questo sostituisce la config v3:
// v3
module.exports = {
darkMode: "class",
};L'approccio @variant è più flessibile. Puoi definire la modalità scura come vuoi — basata su classe, basata su data-attribute, o basata su media query:
/* approccio data attribute */
@variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
/* media query — questo è il default, quindi non devi dichiararlo */
@variant dark (@media (prefers-color-scheme: dark));Compatibilità Turbopack#
Se usi Next.js con Turbopack (che ora è il bundler dev predefinito), la v4 funziona benissimo. Il motore Rust si integra bene con l'architettura basata su Rust di Turbopack. Ho misurato i tempi di avvio dev:
| Setup | v3 + Webpack | v3 + Turbopack | v4 + Turbopack |
|---|---|---|---|
| Avvio a freddo | 4,8s | 2,1s | 1,3s |
| HMR (modifica CSS) | 450ms | 180ms | 40ms |
I 40ms di HMR per le modifiche CSS sono appena percettibili. Sembra istantaneo.
Approfondimento Prestazioni: Oltre la Velocità di Build#
I benefici del motore Oxide vanno oltre la pura velocità di build.
Utilizzo Memoria#
La v4 usa significativamente meno memoria. Sul mio progetto con 847 componenti:
| Metrica | v3 | v4 |
|---|---|---|
| Memoria di picco (build) | 380MB | 45MB |
| Stato stazionario (dev) | 210MB | 28MB |
Questo conta per le pipeline CI/CD dove la memoria è limitata, e per macchine di sviluppo che eseguono dieci processi contemporaneamente.
Dimensione Output CSS#
La v4 genera output CSS leggermente più piccolo perché il nuovo motore è migliore nella deduplica e nell'eliminazione del codice morto:
output v3: 34,2 KB (gzipped)
output v4: 29,8 KB (gzipped)
Una riduzione del 13% senza cambiare alcun codice. Non trasformativo, ma prestazioni gratuite.
Tree Shaking dei Valori del Tema#
Nella v4, se definisci un valore del tema ma non lo usi mai nei tuoi template, la corrispondente custom property CSS viene comunque emessa (è in @theme, che corrisponde alle variabili :root). Tuttavia, le classi utility per i valori inutilizzati non vengono generate. Questo è lo stesso comportamento JIT della v3 ma vale la pena notarlo: le tue custom property CSS sono sempre disponibili, anche per valori senza utilizzo di utility.
Se vuoi impedire a certi valori del tema di generare custom property CSS, puoi usare @theme inline:
@theme inline {
/* Questi valori generano utility ma NON custom property CSS */
--color-internal-debug: #ff00ff;
--spacing-magic-number: 3.7rem;
}Questo è utile per design token interni che non vuoi esporre come variabili CSS.
Avanzato: Comporre Temi per Multi-Brand#
Un pattern che la v4 rende significativamente più facile è il theming multi-brand. Poiché i valori del tema sono custom property CSS, puoi scambiarli a runtime:
@import "tailwindcss";
@theme {
--color-brand: var(--brand-primary, #3b82f6);
--color-brand-light: var(--brand-light, #60a5fa);
--color-brand-dark: var(--brand-dark, #1d4ed8);
}
/* Override dei brand */
.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">
<!-- tutti i bg-brand, text-brand, ecc. usano i colori Acme -->
<div class="bg-brand text-white">Acme Corp</div>
</body>Nella v3, questo richiedeva un plugin personalizzato o un setup complesso di variabili CSS al di fuori di Tailwind. Nella v4, è naturale — il tema sono variabili CSS, e le variabili CSS cascadano. Questo è il tipo di cosa che fa sentire giusto l'approccio CSS-first.
Cosa Mi Manca della v3#
Lasciami essere equilibrato. Ci sono cose della v3 che mi mancano genuinamente nella v4:
1. Config JavaScript per temi programmatici. Avevo un progetto dove generavamo scale di colori da un singolo colore brand usando una funzione JavaScript nella config. Nella v4, non puoi farlo in @theme — avresti bisogno di un passaggio di build che genera il file CSS, o calcoli i colori una volta e li incolli. Il layer di compatibilità @config aiuta, ma non è la storia a lungo termine.
2. IntelliSense era migliore al lancio. L'estensione VS Code della v3 aveva anni di perfezionamento. IntelliSense della v4 funziona ma aveva alcune lacune all'inizio — i valori @theme personalizzati a volte non autocompletavano, e le definizioni @utility non venivano sempre rilevate. Questo è migliorato sostanzialmente con gli aggiornamenti recenti, ma vale la pena notarlo.
3. Maturità dell'ecosistema. L'ecosistema attorno alla v3 era enorme. Headless UI, Radix, shadcn/ui, Flowbite, DaisyUI — tutto era testato contro la v3. Il supporto v4 sta arrivando ma non è universale. Ho dovuto sottomettere un PR a una libreria di componenti per correggere la compatibilità v4.
Dovresti Migrare?#
Ecco il mio framework decisionale dopo aver vissuto con la v4 per diverse settimane:
Migra Ora Se:#
- Stai iniziando un nuovo progetto (scelta ovvia — parti con la v4)
- Il tuo progetto ha plugin personalizzati minimi
- Vuoi i benefici prestazionali per progetti grandi
- Stai già usando pattern moderni di Tailwind (opacità con slash,
shrink-*, ecc.) - Hai bisogno delle container query e preferiresti non aggiungere un plugin
Aspetta Se:#
- Dipendi pesantemente da plugin Tailwind di terze parti che non supportano ancora la v4
- Hai configurazione del tema programmatica complessa
- Il tuo progetto è stabile e non in sviluppo attivo (perché toccarlo?)
- Sei nel mezzo di uno sprint di funzionalità (migra tra uno sprint e l'altro, non durante)
Non Migrare Se:#
- Sei sulla v2 o precedente (aggiorna prima alla v3, stabilizza, poi considera la v4)
- Il tuo progetto termina nei prossimi mesi (non vale il disturbo)
La Mia Opinione Onesta#
Per i nuovi progetti, la v4 è la scelta ovvia. La configurazione CSS-first è più pulita, il motore è drammaticamente più veloce, e le nuove funzionalità (container query, @starting-style, nuove variant) sono genuinamente utili.
Per i progetti esistenti, raccomando un approccio graduale:
- Ora: Inizia qualsiasi nuovo progetto sulla v4
- Presto: Sperimenta convertendo un piccolo progetto interno alla v4
- Quando sei pronto: Migra i progetti in produzione durante uno sprint tranquillo, con test di regressione visiva
La migrazione non è dolorosa se ti prepari. Il codemod gestisce l'80% del lavoro. Il restante 20% è manuale ma diretto. Prevedi un giorno per un progetto medio, da due a tre giorni per uno grande.
Tailwind v4 è ciò che Tailwind avrebbe dovuto essere fin dall'inizio. La configurazione JavaScript è sempre stata una concessione agli strumenti del suo tempo. Configurazione CSS-first, cascade layer nativi, un motore Rust — non sono tendenze, sono il framework che raggiunge la piattaforma. La piattaforma web è migliorata, e Tailwind v4 vi si appoggia invece di combatterla.
Il passaggio a scrivere i tuoi design token in CSS, comporli con funzionalità CSS e lasciare che il meccanismo di cascade del browser gestisca la specificità — questa è la direzione giusta. Ci sono volute quattro versioni major per arrivare qui, ma il risultato è la versione più coerente di Tailwind finora.
Inizia il tuo prossimo progetto con essa. Non ti guarderai indietro.