Tailwind CSS v4 : Ce qui a vraiment changé et faut-il migrer
Configuration CSS-first, intégration @layer, container queries intégrées, performances du nouveau moteur, changements majeurs et mon retour d'expérience honnête sur la migration de v3 vers v4.
J'utilise Tailwind CSS depuis la v1.x, à l'époque où la moitié de la communauté pensait que c'était une abomination et l'autre moitié ne pouvait s'arrêter de livrer avec. Chaque version majeure a été un bond significatif, mais la v4 est différente. Ce n'est pas simplement une mise à jour de fonctionnalités. C'est une réécriture architecturale de fond en comble qui change le contrat fondamental entre vous et le framework.
Après avoir migré deux projets en production de v3 vers v4 et démarré trois nouveaux projets directement sur v4, j'ai une vision claire de ce qui est véritablement mieux, de ce qui est encore brut, et de la question de savoir si vous devriez migrer dès maintenant. Pas de battage, pas d'indignation — juste ce que j'ai observé.
La vision d'ensemble : Ce qu'est réellement la v4#
Tailwind CSS v4 est trois choses à la fois :
- Un nouveau moteur — réécrit de JavaScript vers Rust (le moteur Oxide), rendant les builds considérablement plus rapides
- Un nouveau paradigme de configuration — la configuration CSS-first remplace
tailwind.config.jspar défaut - Une intégration plus étroite avec la plateforme CSS —
@layernatif, container queries,@starting-styleet les cascade layers sont des citoyens de première classe
Le titre que vous verrez partout est « 10x plus rapide ». C'est réel, mais ça sous-estime le véritable changement. Le modèle mental pour configurer et étendre Tailwind a fondamentalement changé. Vous travaillez avec du CSS maintenant, pas un objet de configuration JavaScript qui génère du CSS.
Voici à quoi ressemble une configuration minimale Tailwind v4 :
/* app.css — c'est toute la configuration */
@import "tailwindcss";C'est tout. Pas de fichier de configuration. Pas de configuration de plugin PostCSS (pour la plupart des setups). Plus de directives @tailwind base; @tailwind components; @tailwind utilities;. Un import, et c'est parti.
Comparez avec 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: {},
},
};Trois fichiers réduits à une seule ligne. Ce n'est pas juste moins de code passe-partout — c'est moins de surface d'erreur de configuration. En v4, la détection du contenu est automatique. Le framework scanne les fichiers de votre projet sans que vous ayez besoin de spécifier des patterns glob.
Configuration CSS-First avec @theme#
C'est le plus grand changement conceptuel. En v3, vous personnalisiez Tailwind via un objet de configuration 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",
},
},
},
};En v4, tout cela vit dans le CSS grâce à la directive @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;
}Au début, j'ai résisté à ce changement. J'aimais avoir un seul objet JavaScript où je pouvais voir l'ensemble de mon design system. Mais après une semaine avec l'approche CSS, j'ai changé d'avis pour trois raisons :
1. Les propriétés personnalisées CSS natives sont exposées automatiquement. Chaque valeur que vous définissez dans @theme devient une propriété personnalisée CSS sur :root. Cela signifie que vos valeurs de thème sont accessibles en CSS pur, dans les CSS Modules, dans les balises <style>, partout où le CSS s'exécute :
/* vous obtenez ceci gratuitement */
:root {
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}/* utilisez-les n'importe où — pas besoin de Tailwind */
.custom-element {
border: 2px solid var(--color-brand-500);
}2. Vous pouvez utiliser les fonctionnalités CSS à l'intérieur de @theme. Les media queries, light-dark(), calc() — du vrai CSS fonctionne ici parce que c'est du vrai CSS :
@theme {
--color-surface: light-dark(#ffffff, #0a0a0a);
--color-text: light-dark(#0a0a0a, #fafafa);
--spacing-container: calc(100vw - 2rem);
}3. La co-localisation avec votre autre CSS. Votre thème, vos utilitaires personnalisés et vos styles de base vivent tous dans le même langage, dans le même fichier si vous le souhaitez. Plus besoin de basculer entre le « monde CSS » et le « monde de configuration JavaScript ».
Remplacement vs Extension du thème par défaut#
En v3, vous aviez theme (remplacer) vs theme.extend (fusionner). En v4, le modèle mental est différent :
@import "tailwindcss";
/* Ceci ÉTEND le thème par défaut — ajoute les couleurs brand aux côtés des existantes */
@theme {
--color-brand-500: #3b82f6;
}Si vous voulez remplacer complètement un namespace (comme supprimer toutes les couleurs par défaut), vous utilisez @theme avec la réinitialisation par joker --color-* :
@import "tailwindcss";
@theme {
/* Supprimer d'abord toutes les couleurs par défaut */
--color-*: initial;
/* Maintenant définir uniquement vos couleurs */
--color-white: #ffffff;
--color-black: #000000;
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}Ce pattern de réinitialisation par joker est élégant. Vous choisissez exactement quelles parties du thème par défaut garder et lesquelles remplacer. Vous voulez tout l'espacement par défaut mais des couleurs personnalisées ? Réinitialisez --color-*: initial; et laissez l'espacement tranquille.
Fichiers de thème multiples#
Pour les projets plus importants, vous pouvez répartir votre thème sur plusieurs fichiers :
/* 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";C'est beaucoup plus propre que le pattern v3 consistant à avoir un énorme tailwind.config.js ou à essayer de le scinder avec require().
Le moteur Oxide : C'est vraiment 10x plus rapide#
Le moteur de Tailwind v4 est une réécriture complète en Rust. Ils l'appellent Oxide. J'étais sceptique quant à l'affirmation « 10x plus rapide » — les chiffres marketing survivent rarement au contact avec de vrais projets. J'ai donc fait des benchmarks.
Mon projet de test : Une application Next.js avec 847 composants, 142 pages, environ 23 000 utilisations de classes Tailwind.
| Métrique | v3 (Node) | v4 (Oxide) | Amélioration |
|---|---|---|---|
| Build initial | 4 280ms | 387ms | 11x |
| Incrémental (modifier 1 fichier) | 340ms | 18ms | 19x |
| Rebuild complet (clean) | 5 100ms | 510ms | 10x |
| Démarrage du serveur dev | 3 200ms | 290ms | 11x |
L'affirmation « 10x » est conservatrice pour mon projet. Les builds incrémentaux sont là où ça brille vraiment — 18ms signifie que c'est essentiellement instantané. Vous sauvegardez un fichier et le navigateur a les nouveaux styles avant que vous puissiez changer d'onglet.
Pourquoi est-ce tellement plus rapide ?#
Trois raisons :
1. Rust au lieu de JavaScript. Le parseur CSS central, la détection de classes et la génération de code sont tous en Rust natif. Ce n'est pas une situation « réécrivons en Rust pour le plaisir » — le parsing CSS est un travail véritablement gourmand en CPU où le code natif a un avantage massif sur V8.
2. Plus de PostCSS dans le chemin critique. En v3, Tailwind était un plugin PostCSS. Chaque build signifiait : parser le CSS en AST PostCSS, exécuter le plugin Tailwind, sérialiser en chaîne CSS, puis les autres plugins PostCSS s'exécutent. En v4, Tailwind a son propre parseur CSS qui va directement de la source à la sortie. PostCSS est toujours supporté pour la compatibilité, mais le chemin principal le contourne entièrement.
3. Un traitement incrémental plus intelligent. Le nouveau moteur met en cache de manière agressive. Quand vous modifiez un seul fichier, il ne re-scanne que ce fichier pour les noms de classes et ne régénère que les règles CSS qui ont changé. Le moteur v3 était plus intelligent à ce sujet que les gens ne lui accordent (le mode JIT était déjà incrémental), mais la v4 va beaucoup plus loin avec un suivi fin des dépendances.
La vitesse importe-t-elle vraiment ?#
Oui, mais pas pour la raison que vous imaginez. Pour la plupart des projets, la vitesse de build v3 était « correcte ». Vous attendiez quelques centaines de millisecondes en dev. Pas douloureux.
La vitesse v4 importe parce qu'elle rend Tailwind invisible dans votre chaîne d'outils. Quand les builds prennent moins de 20ms, vous cessez de penser à Tailwind comme une étape de build. Cela devient comme la coloration syntaxique — toujours là, jamais dans le chemin. Cette différence psychologique est significative sur une journée complète de développement.
Intégration native de @layer#
En v3, Tailwind utilisait son propre système de couches avec @layer base, @layer components et @layer utilities. Ceux-ci ressemblaient à des cascade layers CSS mais n'en étaient pas — c'étaient des directives spécifiques à Tailwind qui contrôlaient où le CSS généré apparaissait dans la sortie.
En v4, Tailwind utilise de véritables cascade layers CSS :
/* sortie v4 — simplifiée */
@layer theme, base, components, utilities;
@layer base {
/* reset, preflight */
}
@layer components {
/* vos classes de composants */
}
@layer utilities {
/* toutes les classes utilitaires générées */
}C'est un changement significatif car les cascade layers CSS ont de véritables implications de spécificité. Une règle dans une couche de priorité inférieure perd toujours face à une règle dans une couche de priorité supérieure, indépendamment de la spécificité du sélecteur. Cela signifie :
@layer components {
/* spécificité : 0-1-0 */
.card { padding: 1rem; }
}
@layer utilities {
/* spécificité : 0-1-0 — même spécificité mais gagne car la couche utilities est ultérieure */
.p-4 { padding: 1rem; }
}Les utilitaires l'emportent toujours sur les composants. Les composants l'emportent toujours sur la base. C'est ainsi que Tailwind fonctionnait conceptuellement en v3, mais maintenant c'est appliqué par le mécanisme de cascade layers du navigateur, pas par une manipulation de l'ordre du source.
Ajout d'utilitaires personnalisés#
En v3, vous définissiez des utilitaires personnalisés avec une API de plugin ou @layer utilities :
// v3 — approche plugin
const plugin = require("tailwindcss/plugin");
module.exports = {
plugins: [
plugin(function ({ addUtilities }) {
addUtilities({
".text-balance": {
"text-wrap": "balance",
},
".text-pretty": {
"text-wrap": "pretty",
},
});
}),
],
};En v4, les utilitaires personnalisés sont définis avec la directive @utility :
@import "tailwindcss";
@utility text-balance {
text-wrap: balance;
}
@utility text-pretty {
text-wrap: pretty;
}La directive @utility indique à Tailwind « ceci est une classe utilitaire — place-la dans la couche utilities et permets son utilisation avec les variants ». Cette dernière partie est essentielle. Un utilitaire défini avec @utility fonctionne automatiquement avec hover:, focus:, md: et tous les autres variants :
<p class="text-pretty md:text-balance">...</p>Variants personnalisés#
Vous pouvez également définir des variants personnalisés avec @variant :
@import "tailwindcss";
@variant hocus (&:hover, &:focus);
@variant theme-dark (.dark &);<button class="hocus:bg-brand-500 theme-dark:text-white">
Cliquez ici
</button>Cela remplace l'API de plugin addVariant de la v3 pour la plupart des cas d'usage. C'est moins puissant (vous ne pouvez pas faire de génération programmatique de variants), mais cela couvre 90% de ce que les gens font réellement.
Container Queries : Intégrées, sans plugin#
Les container queries étaient l'une des fonctionnalités les plus demandées en v3. Vous pouviez les obtenir avec le plugin @tailwindcss/container-queries, mais c'était un ajout. En v4, elles sont intégrées au framework.
Utilisation basique#
Marquez un conteneur avec @container et interrogez sa taille avec le préfixe @ :
<!-- marquer le parent comme conteneur -->
<div class="@container">
<!-- responsive par rapport à la largeur du parent, pas du viewport -->
<div class="flex flex-col @md:flex-row @lg:grid @lg:grid-cols-3">
<div class="p-4">Carte 1</div>
<div class="p-4">Carte 2</div>
<div class="p-4">Carte 3</div>
</div>
</div>Les variants @md, @lg, etc. fonctionnent comme des breakpoints responsive mais sont relatifs au plus proche ancêtre @container plutôt qu'au viewport. Les valeurs de breakpoint correspondent aux breakpoints par défaut de Tailwind :
| Variant | Min-width |
|---|---|
@sm | 24rem (384px) |
@md | 28rem (448px) |
@lg | 32rem (512px) |
@xl | 36rem (576px) |
@2xl | 42rem (672px) |
Conteneurs nommés#
Vous pouvez nommer les conteneurs pour interroger des ancêtres spécifiques :
<div class="@container/sidebar">
<div class="@container/card">
<!-- interroge le conteneur card -->
<div class="@md/card:text-lg">...</div>
<!-- interroge le conteneur sidebar -->
<div class="@lg/sidebar:hidden">...</div>
</div>
</div>Pourquoi c'est important#
Les container queries changent votre façon de penser le design responsive. Au lieu de « à cette largeur de viewport, afficher trois colonnes », vous dites « quand le conteneur de ce composant est assez large, afficher trois colonnes ». Les composants deviennent véritablement autonomes. Vous pouvez déplacer un composant carte d'une mise en page pleine largeur vers une barre latérale et il s'adapte automatiquement. Pas de gymnastique de media queries.
J'ai refactorisé mes bibliothèques de composants pour utiliser les container queries par défaut au lieu des breakpoints du viewport. Le résultat est des composants qui fonctionnent partout où vous les placez, sans que le parent n'ait besoin de connaître quoi que ce soit du comportement responsive du composant.
<!-- Ce composant s'adapte à N'IMPORTE QUEL conteneur dans lequel il est placé -->
<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">Titre de l'article</h2>
<p class="mt-2 text-sm @md:text-base text-gray-600">
L'extrait de l'article va ici...
</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>Nouveaux variants qui comptent vraiment#
La v4 ajoute plusieurs nouveaux variants que j'utilise constamment. Ils comblent de vrais manques.
Le variant starting:#
Celui-ci correspond au @starting-style CSS, qui permet de définir l'état initial d'un élément quand il apparaît pour la première fois. C'est la pièce manquante pour animer l'entrée d'un élément sans JavaScript :
<dialog class="opacity-0 starting:opacity-0 open:opacity-100 transition-opacity duration-300">
<p>Cette boîte de dialogue apparaît en fondu à l'ouverture</p>
</dialog>Le variant starting: génère du CSS à l'intérieur d'un bloc @starting-style :
/* ce que Tailwind génère */
@starting-style {
dialog[open] {
opacity: 0;
}
}
dialog[open] {
opacity: 1;
transition: opacity 300ms;
}C'est énorme pour les dialogues, les popovers, les menus déroulants — tout ce qui nécessite une animation d'entrée. Avant cela, vous aviez besoin de JavaScript pour ajouter une classe sur la frame suivante, ou vous utilisiez @keyframes. Maintenant c'est une classe utilitaire.
Le variant not-*#
La négation. Quelque chose que nous voulions depuis toujours :
<!-- chaque enfant sauf le dernier reçoit une bordure -->
<div class="divide-y">
<div class="not-last:pb-4">Élément 1</div>
<div class="not-last:pb-4">Élément 2</div>
<div class="not-last:pb-4">Élément 3</div>
</div>
<!-- styliser tout ce qui n'est pas désactivé -->
<input class="not-disabled:hover:border-brand-500" />
<!-- nier les attributs data -->
<div class="not-data-active:opacity-50">...</div>Les variants nth-*#
Accès direct à nth-child et nth-of-type :
<ul>
<li class="nth-1:font-bold">Premier élément — gras</li>
<li class="nth-even:bg-gray-50">Lignes paires — fond gris</li>
<li class="nth-odd:bg-white">Lignes impaires — fond blanc</li>
<li class="nth-[3n+1]:text-brand-500">Chaque troisième+1 — couleur brand</li>
</ul>La syntaxe entre crochets (nth-[3n+1]) supporte n'importe quelle expression nth-child valide. Cela remplace beaucoup de CSS personnalisé que j'écrivais auparavant pour les alternances de tableaux et les motifs de grille.
Le variant in-* (État du parent)#
C'est l'inverse de group-*. Au lieu de « quand mon parent (group) est survolé, me styliser », c'est « quand je suis à l'intérieur d'un parent qui correspond à cet état, me styliser » :
<div class="in-data-active:bg-brand-50">
Ceci reçoit un arrière-plan quand un ancêtre a data-active
</div>Le variant **: universel profond#
Styliser tous les descendants, pas seulement les enfants directs. C'est de la puissance contrôlée — utilisez-le avec parcimonie, mais c'est inestimable pour le contenu prose et les sorties CMS :
<!-- tous les paragraphes à l'intérieur de ce div, à n'importe quelle profondeur -->
<div class="**:data-highlight:bg-yellow-100">
<section>
<p data-highlight>Ceci est mis en surbrillance</p>
<div>
<p data-highlight>Ceci aussi, imbriqué plus profondément</p>
</div>
</section>
</div>Changements majeurs : Ce qui a vraiment cassé#
Soyons directs. Si vous avez un gros projet v3, la migration n'est pas triviale. Voici ce qui a cassé dans mes projets :
1. Format de configuration#
Votre tailwind.config.js ne fonctionne pas directement. Vous devez soit :
- Le convertir en CSS
@theme(recommandé pour la nouvelle architecture) - Utiliser la couche de compatibilité avec la directive
@config(chemin de migration rapide)
/* migration rapide — garder votre ancienne config */
@import "tailwindcss";
@config "../../tailwind.config.js";Ce pont @config fonctionne, mais c'est explicitement un outil de migration. La recommandation est de passer à @theme progressivement.
2. Utilitaires obsolètes supprimés#
Certains utilitaires qui étaient dépréciés en v3 ont disparu :
/* SUPPRIMÉS en v4 */
bg-opacity-* → utiliser bg-black/50 (syntaxe slash pour l'opacité)
text-opacity-* → utiliser text-black/50
border-opacity-* → utiliser border-black/50
flex-shrink-* → utiliser shrink-*
flex-grow-* → utiliser grow-*
overflow-ellipsis → utiliser text-ellipsis
decoration-slice → utiliser box-decoration-slice
decoration-clone → utiliser box-decoration-clone
Si vous utilisiez déjà la syntaxe moderne en v3 (slash pour l'opacité, shrink-*), c'est bon. Sinon, ce sont des changements simples de rechercher-remplacer.
3. Changements de la palette de couleurs par défaut#
La palette de couleurs par défaut a légèrement changé. Si vous dépendez des valeurs exactes de couleur de v3 (pas par nom mais par la valeur hexadécimale réelle), vous pourriez remarquer des différences visuelles. Les couleurs nommées (blue-500, gray-200) existent toujours mais certaines valeurs hexadécimales ont changé.
4. Détection du contenu#
La v3 nécessitait une configuration explicite du content :
// v3
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
};La v4 utilise la détection automatique du contenu. Elle scanne la racine de votre projet et trouve les fichiers de template automatiquement. Cela « fonctionne tout simplement » dans la plupart des cas, mais si vous avez une structure de projet inhabituelle (monorepo avec des packages en dehors de la racine du projet, fichiers de template à des emplacements inattendus), vous devrez peut-être configurer les chemins source explicitement :
@import "tailwindcss";
@source "../shared-components/**/*.tsx";
@source "../design-system/src/**/*.tsx";5. Changements de l'API de plugins#
Si vous avez écrit des plugins personnalisés, l'API a changé. Les fonctions addUtilities, addComponents, addBase et addVariant fonctionnent toujours via la couche de compatibilité, mais l'approche idiomatique v4 est native CSS :
// plugin v3
plugin(function ({ addUtilities, theme }) {
addUtilities({
".scrollbar-hide": {
"-ms-overflow-style": "none",
"scrollbar-width": "none",
"&::-webkit-scrollbar": {
display: "none",
},
},
});
});/* v4 — juste du CSS */
@utility scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}La plupart des plugins de première partie (@tailwindcss/typography, @tailwindcss/forms, @tailwindcss/aspect-ratio) ont des versions compatibles v4. Les plugins tiers, c'est variable — vérifiez leur dépôt avant de migrer.
6. Le JIT est le seul mode#
En v3, vous pouviez désactiver le mode JIT (bien que presque personne ne le faisait). En v4, il n'y a pas de mode non-JIT. Tout est généré à la demande, toujours. Si vous aviez une raison d'utiliser l'ancien moteur AOT (ahead-of-time), ce chemin n'existe plus.
7. Certains changements de syntaxe de variants#
Quelques variants ont été renommés ou ont changé de comportement :
<!-- v3 -->
<div class="[&>*]:p-4">...</div>
<!-- v4 — la partie >* utilise maintenant la syntaxe de variant inset -->
<div class="*:p-4">...</div>La syntaxe de variant arbitraire [&...] fonctionne toujours, mais v4 fournit des alternatives nommées pour les patterns courants.
Guide de migration : Le vrai processus#
Voici comment j'ai réellement migré, pas le chemin idéal de la documentation mais à quoi le processus a vraiment ressemblé.
Étape 1 : Exécuter le codemod officiel#
Tailwind fournit un codemod qui gère la plupart des changements mécaniques :
npx @tailwindcss/upgradeCela fait beaucoup automatiquement :
- Convertit les directives
@tailwinden@import "tailwindcss" - Renomme les classes utilitaires dépréciées
- Met à jour la syntaxe des variants
- Convertit les utilitaires d'opacité en syntaxe slash (
bg-opacity-50enbg-black/50) - Crée un bloc
@themebasique à partir de votre config
Ce que le codemod gère bien#
- Les renommages de classes utilitaires (quasi parfait)
- Les changements de syntaxe de directives
- Les valeurs de thème simples (couleurs, espacement, polices)
- La migration de syntaxe d'opacité
Ce que le codemod NE gère PAS#
- Les conversions de plugins complexes
- Les valeurs de config dynamiques (appels
theme()en JavaScript) - La configuration de thème conditionnelle (par ex. valeurs de thème basées sur l'environnement)
- Les migrations d'API de plugins personnalisés
- Les cas limites de valeurs arbitraires où le nouveau parseur interprète différemment
- Les noms de classes construits dynamiquement en JavaScript (template literals, concaténation de chaînes)
Étape 2 : Corriger la configuration PostCSS#
Pour la plupart des setups, vous mettrez à jour votre config PostCSS :
// postcss.config.js — v4
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
};Note : le nom du plugin a changé de tailwindcss à @tailwindcss/postcss. Si vous utilisez Vite, vous pouvez sauter PostCSS entièrement et utiliser le plugin Vite :
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});Étape 3 : Convertir la configuration du thème#
C'est la partie manuelle. Prenez les valeurs de thème de votre tailwind.config.js et convertissez-les en @theme :
// config v3 — avant
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 — après */
@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; }
}Notez que les keyframes sortent de @theme et deviennent des @keyframes CSS classiques. Le nom d'animation dans @theme les référence simplement. C'est plus propre — les keyframes sont du CSS, ils devraient être écrits en CSS.
Étape 4 : Tests de régression visuelle#
C'est non négociable. Après la migration, j'ai ouvert chaque page de mon application et l'ai vérifiée visuellement. J'ai aussi exécuté mes tests de captures d'écran Playwright (si vous en avez). Le codemod est bon mais pas parfait. Ce que j'ai détecté en revue visuelle :
- Quelques endroits où la migration de syntaxe d'opacité a produit des résultats légèrement différents
- Des sorties de plugins personnalisés qui ne se sont pas transférées
- Des changements d'empilement z-index dus à l'ordonnancement des couches
- Certaines surcharges
!importantqui se comportaient différemment avec les cascade layers
Étape 5 : Mettre à jour les dépendances tierces#
Vérifiez chaque package lié à Tailwind :
{
"@tailwindcss/typography": "^1.0.0",
"@tailwindcss/forms": "^1.0.0",
"@tailwindcss/container-queries": "SUPPRIMER — intégré maintenant",
"tailwindcss-animate": "vérifier le support v4",
"prettier-plugin-tailwindcss": "mettre à jour vers la dernière version"
}Le plugin @tailwindcss/container-queries n'est plus nécessaire — les container queries sont intégrées. Les autres plugins ont besoin de leurs versions compatibles v4.
Travailler avec Next.js#
Puisque j'utilise Next.js pour la plupart de mes projets, voici le setup spécifique.
Approche PostCSS (recommandée pour Next.js)#
Next.js utilise PostCSS sous le capot, donc le plugin PostCSS est le choix naturel :
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;
}C'est le setup complet. Pas de tailwind.config.js, pas d'autoprefixer (la v4 gère les préfixes vendeurs en interne).
Ordre d'import CSS#
Un point qui m'a piégé : l'ordre d'import CSS compte davantage en v4 à cause des cascade layers. Votre @import "tailwindcss" devrait venir avant vos styles personnalisés :
/* ordre correct */
@import "tailwindcss";
@import "./theme.css";
@import "./custom-utilities.css";
/* vos @theme, @utility, etc. en ligne */Si vous importez du CSS personnalisé avant Tailwind, vos styles pourraient se retrouver dans une cascade layer de priorité inférieure et être surchargés de manière inattendue.
Mode sombre#
Le mode sombre fonctionne de la même manière conceptuellement mais la configuration a migré vers le CSS :
@import "tailwindcss";
/* Utiliser le mode sombre basé sur la classe (le défaut est basé sur media) */
@variant dark (&:where(.dark, .dark *));Cela remplace la config v3 :
// v3
module.exports = {
darkMode: "class",
};L'approche @variant est plus flexible. Vous pouvez définir le mode sombre comme vous le souhaitez — basé sur une classe, un attribut data, ou une media query :
/* approche par attribut data */
@variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
/* media query — c'est le défaut, donc vous n'avez pas besoin de le déclarer */
@variant dark (@media (prefers-color-scheme: dark));Compatibilité Turbopack#
Si vous utilisez Next.js avec Turbopack (qui est maintenant le bundler dev par défaut), la v4 fonctionne très bien. Le moteur Rust s'harmonise bien avec l'architecture propre de Turbopack, elle aussi basée sur Rust. J'ai mesuré les temps de démarrage en dev :
| Configuration | v3 + Webpack | v3 + Turbopack | v4 + Turbopack |
|---|---|---|---|
| Démarrage à froid | 4,8s | 2,1s | 1,3s |
| HMR (changement CSS) | 450ms | 180ms | 40ms |
Le HMR de 40ms pour les changements CSS est à peine perceptible. Cela paraît instantané.
Analyse approfondie des performances : Au-delà de la vitesse de build#
Les avantages du moteur Oxide vont au-delà de la vitesse de build brute.
Utilisation mémoire#
La v4 utilise significativement moins de mémoire. Sur mon projet de 847 composants :
| Métrique | v3 | v4 |
|---|---|---|
| Mémoire pic (build) | 380 Mo | 45 Mo |
| État stable (dev) | 210 Mo | 28 Mo |
Cela compte pour les pipelines CI/CD où la mémoire est limitée, et pour les machines de développement qui exécutent dix processus simultanément.
Taille de la sortie CSS#
La v4 génère une sortie CSS légèrement plus petite car le nouveau moteur est meilleur en déduplication et élimination du code mort :
Sortie v3 : 34,2 Ko (gzippé)
Sortie v4 : 29,8 Ko (gzippé)
Une réduction de 13% sans changer de code. Pas transformatif, mais des performances gratuites.
Tree Shaking des valeurs de thème#
En v4, si vous définissez une valeur de thème mais ne l'utilisez jamais dans vos templates, la propriété personnalisée CSS correspondante est quand même émise (elle est dans @theme, qui correspond aux variables :root). Cependant, les classes utilitaires pour les valeurs non utilisées ne sont pas générées. C'est le même comportement que le JIT v3 mais cela vaut la peine d'être noté : vos propriétés personnalisées CSS sont toujours disponibles, même pour les valeurs sans utilisation utilitaire.
Si vous voulez empêcher certaines valeurs de thème de générer des propriétés personnalisées CSS, vous pouvez utiliser @theme inline :
@theme inline {
/* Ces valeurs génèrent des utilitaires mais PAS de propriétés personnalisées CSS */
--color-internal-debug: #ff00ff;
--spacing-magic-number: 3.7rem;
}C'est utile pour les design tokens internes que vous ne voulez pas exposer comme variables CSS.
Avancé : Composition de thèmes multi-marque#
Un pattern que la v4 rend significativement plus facile est le theming multi-marque. Parce que les valeurs de thème sont des propriétés personnalisées CSS, vous pouvez les échanger au runtime :
@import "tailwindcss";
@theme {
--color-brand: var(--brand-primary, #3b82f6);
--color-brand-light: var(--brand-light, #60a5fa);
--color-brand-dark: var(--brand-dark, #1d4ed8);
}
/* Surcharges de marque */
.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">
<!-- tous les bg-brand, text-brand, etc. utilisent les couleurs Acme -->
<div class="bg-brand text-white">Acme Corp</div>
</body>En v3, cela nécessitait un plugin personnalisé ou une configuration complexe de variables CSS en dehors de Tailwind. En v4, c'est naturel — le thème est constitué de variables CSS, et les variables CSS se propagent en cascade. C'est le genre de chose qui rend l'approche CSS-first pertinente.
Ce qui me manque de la v3#
Soyons équilibrés. Il y a des choses que la v3 faisait que je regrette véritablement en v4 :
1. La config JavaScript pour les thèmes programmatiques. J'avais un projet où nous générions des échelles de couleur à partir d'une seule couleur de marque en utilisant une fonction JavaScript dans la config. En v4, vous ne pouvez pas faire cela dans @theme — vous auriez besoin d'une étape de build qui génère le fichier CSS, ou vous calculeriez les couleurs une fois et les colleriez dedans. La couche de compatibilité @config aide, mais ce n'est pas la vision à long terme.
2. L'IntelliSense était meilleur au lancement. L'extension VS Code v3 avait des années de peaufinage. L'IntelliSense v4 fonctionne mais avait quelques lacunes au début — les valeurs @theme personnalisées ne s'auto-complétaient pas toujours, et les définitions @utility n'étaient pas toujours détectées. Cela s'est considérablement amélioré avec les mises à jour récentes, mais c'est à noter.
3. Maturité de l'écosystème. L'écosystème autour de v3 était énorme. Headless UI, Radix, shadcn/ui, Flowbite, DaisyUI — tout était testé contre v3. Le support v4 se déploie mais n'est pas universel. J'ai dû soumettre une PR à une bibliothèque de composants pour corriger la compatibilité v4.
Devriez-vous migrer ?#
Voici mon cadre de décision après avoir vécu avec la v4 pendant plusieurs semaines :
Migrez maintenant si :#
- Vous démarrez un nouveau projet (choix évident — commencez avec v4)
- Votre projet a un minimum de plugins personnalisés
- Vous voulez les gains de performance pour les gros projets
- Vous utilisez déjà les patterns Tailwind modernes (slash pour l'opacité,
shrink-*, etc.) - Vous avez besoin des container queries et préférez ne pas ajouter de plugin
Attendez si :#
- Vous dépendez fortement de plugins Tailwind tiers qui ne supportent pas encore la v4
- Vous avez une configuration de thème programmatique complexe
- Votre projet est stable et n'est pas activement développé (pourquoi y toucher ?)
- Vous êtes en plein sprint de fonctionnalités (migrez entre les sprints, pas pendant)
Ne migrez pas si :#
- Vous êtes sur la v2 ou antérieure (mettez d'abord à jour vers v3, stabilisez, puis envisagez v4)
- Votre projet se termine dans les prochains mois (le changement n'en vaut pas la peine)
Mon avis honnête#
Pour les nouveaux projets, v4 est le choix évident. La configuration CSS-first est plus propre, le moteur est considérablement plus rapide, et les nouvelles fonctionnalités (container queries, @starting-style, nouveaux variants) sont véritablement utiles.
Pour les projets existants, je recommande une approche progressive :
- Maintenant : Commencez tout nouveau projet sur v4
- Bientôt : Expérimentez en convertissant un petit projet interne vers v4
- Quand vous êtes prêt : Migrez les projets en production pendant un sprint calme, avec des tests de régression visuelle
La migration n'est pas douloureuse si vous vous y préparez. Le codemod gère 80% du travail. Les 20% restants sont manuels mais directs. Prévoyez une journée pour un projet moyen, deux à trois jours pour un gros projet.
Tailwind v4 est ce que Tailwind aurait dû être depuis le début. La configuration JavaScript a toujours été une concession aux outils de son époque. Configuration CSS-first, cascade layers natives, moteur Rust — ce ne sont pas des modes, c'est le framework qui rattrape la plateforme. La plateforme web s'est améliorée, et Tailwind v4 s'appuie dessus au lieu de lutter contre.
Le passage à l'écriture de vos design tokens en CSS, à leur composition avec les fonctionnalités CSS, et à laisser la propre cascade du navigateur gérer la spécificité — c'est la bonne direction. Il a fallu quatre versions majeures pour y arriver, mais le résultat est la version de Tailwind la plus cohérente à ce jour.
Commencez votre prochain projet avec. Vous ne regarderez pas en arrière.