Nous avons migré notre plateforme Symfony/Vue 3 de Tailwind CSS v3.2 vers v4 : un design system centralisé dans un bundle (80+ composants Vue) consommé par plusieurs projets clients. Cet article restitue les changements factuels constatés, puis la nouvelle organisation du thème que la v4 permet entre un bundle central et ses projets.
Sommaire
- Pourquoi faire cette migration ?
- Comprendre la nouvelle configuration en CSS
- Les impacts CSS constatés sur nos projets
- Déclarer les variables de thème : bundle central, config projet
- Surcharger le thème : par interface, par zone, par composant
- Responsive et media queries
- Profiter des nouveautés depuis la migration
- Conclusion
- Liens utiles
1. Pourquoi faire cette migration ?
Tailwind CSS v4 est devenu le standard front de l’écosystème : c’est la version sur laquelle s’alignent les templates et composants fournis par Tailwind UI, NuxtUI, PrimeVue et les autres librairies basées sur Tailwind. Rester en v3, c’est s’éloigner progressivement de ces ressources et ralentir l’intégration de tout template ou composant communautaire.
Au-delà de la compatibilité écosystème, la v4 apporte des gains concrets :
- Future version LTS : la v4 est la branche qui recevra le support long terme ; la v3 n’évoluera plus.
- Simplification de la configuration : le
tailwind.config.jsdisparaît au profit d’un fichier CSS unique (@theme,@source,@plugin). Moins de fichiers à maintenir, moins de synchronisation entre projets. - Moins de dépendances de preprocessing : PostCSS, Sass, Autoprefixer et Babel ne sont plus nécessaires, la v4 intègre nativement le traitement des imports, le nesting, le préfixage et la transpilation. Autoprefixer disparaît précisément parce que Lightning CSS prend le relais en interne : préfixage vendeur automatique et transformations de syntaxe moderne (nesting, custom media queries, propriétés logiques, espaces colorimétriques étendus). C’est autant de surface de maintenance et de CVE potentielles en moins.
Un point de vigilance avant de se lancer : la v4 s’appuie sur des fonctionnalités CSS récentes (@property,
cascade layers natifs, color-mix()) et cible officiellement, selon la
documentation de compatibilité, Chrome 111 (sorti en mars 2023),
Safari 16.4 (sorti en mars 2023) et Firefox 128 (sorti en juillet 2024), sans dégradation gracieuse vers les
moteurs plus anciens. Edge n’est pas cité par la documentation, mais partage le moteur Chromium de Chrome : Edge
111 est sorti la même semaine, en mars 2023, et couvre donc le même socle de fonctionnalités.
Cette base minimale ne couvre que le cœur du framework. Pour les fonctionnalités les plus récentes qu’elle expose
en variant optionnel, comme starting: (basé sur @starting-style, pour des animations d’apparition sans JS), la
barre monte : caniuse situe le support à Chrome 117, Edge
117, Firefox 129, Safari 17.5 et Opera 103, tous plus récents que le socle Chrome/Safari/Firefox annoncé par
Tailwind. La documentation officielle le précise elle-même : vérifiez sur caniuse la fonctionnalité précise que
vous comptez utiliser avant de l’adopter, la base 111/16.4/128 garantit le fonctionnement du moteur, pas celui de
chaque variant bleeding-edge pris individuellement.
Sur nos projets Symfony/Vue destinés à des utilisateurs professionnels sur navigateurs à jour, ça n’a jamais posé de problème, mais c’est un prérequis à vérifier avant toute migration si une partie de votre parc utilisateur tourne encore sur un navigateur d’entreprise ancien.
C’est aussi ce qui explique notre calendrier : Tailwind CSS v4 a été annoncé officiellement en janvier 2025, mais nous n’avons officialisé la migration sur nos projets qu’en 2026. Le temps que le socle de navigateurs requis soit réellement installé chez nos clients (renouvellement du parc, mises à jour d’entreprise souvent plus lentes que le grand public) et que l’écosystème de plugins se stabilise autour de la config CSS-first, mieux valait laisser une version tourner en conditions réelles ailleurs avant de l’appliquer à un design system consommé par plusieurs projets clients.
2. Comprendre la nouvelle configuration en CSS
Le changement structurant de la v4 : plus de tailwind.config.js.
La configuration (thème, sources à scanner, safelist, plugins) vit directement dans le fichier CSS d’entrée.
Avant (v3) : deux fichiers JS + des directives
// tailwind.config.js
module.exports = {
content: [
'templates/**/*.html.twig',
'vendor/smartbooster/**/*.vue',
'assets/admin/scripts/**/*.vue',
],
theme: {
extend: {
colors: {
primary: {
lighter: 'var(--primary-lighter)',
DEFAULT: 'var(--primary-default)',
dark: 'var(--primary-dark)',
// ...
},
},
},
},
safelist: [
'grid-cols-2', 'grid-cols-3', /* ... ~X entrées ... */
],
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
],
}
// main.scss
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "base";
@import "../../../vendor/smartbooster/platform-core-bundle/assets/styles/base";
Après (v4) : un seul fichier CSS
Les trois directives @tailwind sont remplacées par un import global, et chaque entrée du config JS a son équivalent
CSS : content → @source, safelist → @source inline(), plugins → @plugin, theme → @theme.
/* main.css */
@import "../../../vendor/smartbooster/platform-core-bundle/assets/styles/base.css";
@import "../../../vendor/smartbooster/platform-core-bundle/assets/styles/theme.css";
@import "base.css";
@import "tailwindcss";
@plugin "@tailwindcss/typography";
/* Sources explicites (vendor/ est gitignoré, donc hors détection automatique) */
@source "../../../assets";
@source "../../../templates";
@source "../../../vendor/smartbooster/platform-core-bundle/assets/scripts";
@source "../../../vendor/smartbooster/platform-core-bundle/templates";
/* Équivalent de la safelist v3, avec expansion d'accolades */
@source inline("grid-cols-{2,3,4,5,6,7,8,9,10,11,12,13}");
@source inline("{text,bg}-{success,warning,danger,info}{,-light,-dark}");
/* ... */
Détail des subtilités :
-
@import "tailwindcss"se déplie en couches natives (@layer theme, base, components, utilities). La priorité des styles est fixée par la déclaration des layers, pas par l’ordre physique des imports : on peut importer ses styles avant ou après Tailwind sans changer le rendu. -
La détection des sources est automatique (elle respecte le
.gitignore) ;@sourcene sert qu’à ajouter des chemins hors radar, comme un bundle dansvendor/. Cibler des répertoires précis : un glob large survendor/fait scanner des milliers de fichiers inutiles, c’est pourquoi il est préférable de cibler uniquement notre librairie. -
La safelist v3 (tableau JS) devient
@source inline()avec expansion d’accolades, beaucoup plus compacte. -
Les utilitaires custom définis avec
@layer utilitiesdoivent migrer vers@utilitypour bénéficier des variants (hover:,lg:, etc.) :@utility tab-4 { tab-size: 4; }. Les classes de composants (@layer components) restent inchangées. -
@utilityaccepte aussi un nom en*pour générer une famille entière de classes à la volée, sans les déclarer une par une.--value()capture la valeur passée dans la classe :@utility stack-* { display: flex; flex-direction: column; gap: calc(var(--spacing) * --value(integer)); }stack-4génère alorsgap: calc(var(--spacing) * 4)à la demande, commetab-4mais sans avoir à écrirestack-1,stack-2,stack-4, etc. à l’avance. -
Techniquement PostCSS est toujours supporté via
@tailwindcss/postcss(l’ancien plugintailwindcss: {}est remplacé, pas PostCSS lui-même). Si le projet n’utilisait PostCSS que pour Tailwind,postcss.config.jsetautoprefixerpeuvent être supprimés ; sinon, il faut juste mettre à jour le plugin. -
La v4 est pensée CSS natif et s’accommode mal de Sass : la migration a été l’occasion de convertir nos
.scssen.css(nesting natif,var()à la place des variables Sass).
3. Les impacts CSS constatés sur nos projets
Le guide de migration officiel liste tous les changements. Voici ceux qui ont réellement impacté notre codebase, constatés lors de recette visuelle.
Couleur de bordure par défaut : currentColor
En v3, border, divide-* et ring sans couleur explicite héritaient de gray-200.
En v4, la valeur par défaut est currentColor : toute bordure non colorée devient de la couleur du texte.
Exemple de correction :
- <table class="listing min-w-full divide-y border-t">
+ <table class="listing min-w-full divide-y divide-gray-200 border-t border-gray-200">
ring : largeur et couleur par défaut changées
ring sans taille ni couleur change de comportement en v4, un changement silencieux :
| v3 | v4 | |
|---|---|---|
| Largeur | 3px | 1px (ring = ring-1) |
| Couleur | blue-500 | currentColor |
- <button class="ring ring-blue-500">
+ <button class="ring-3 ring-blue-500">
Un ring sans couleur explicite qui rendait bleu en v3 devient transparent ou prend la couleur du texte en v4.
L’échelle des ombres, arrondis, et flous est décalée
La v4 renomme les échelles pour introduire des tailles plus petites : l’ancien shadow-sm s’appelle désormais
shadow-xs, l’ancien shadow devient shadow-sm, etc. Même logique pour rounded-sm/rounded-xs et
blur-sm/blur-xs.
Les classes shadow-sm existantes compilent toujours, mais avec la nouvelle valeur, plus marquée. C’est un
changement silencieux : pas d’erreur de build, juste un rendu différent.
- <input class="form-input border border-gray-300 rounded-md shadow-sm" />
+ <input class="form-input border border-gray-300 rounded-md shadow-xs" />
Nous en avons profité pour recalibrer visuellement toute l’échelle (menus déroulants shadow-lg → shadow-md,
modales shadow-xl → shadow-lg) : à valider écran par écran, pas de règle automatique.
Le modificateur ! passe de préfixe à suffixe
En v3, !important s’appliquait avec ! avant la classe. En v4, il se place après :
- <div class="!flex !bg-red-500">
+ <div class="flex! bg-red-500!">
Pour @apply, la forme v3 @apply p-0 !important (ou #{!important} en Sass) ne compile plus. La recommandation v4 est d’utiliser du CSS natif directement :
- @apply p-0 #{!important};
+ padding: 0 !important;
Ou, si @apply est incontournable : @apply p-0!; (suffixe ! sur chaque utilitaire).
Le line-height hérité devient un ratio
En v3, text-sm posait un line-height en longueur fixe (1.25rem), héritée telle quelle.
En v4, il est exprimé relativement à la font-size : un pseudo-élément avec sa propre font-size recalcule le ratio et change de hauteur.
Nos labels de formulaire avec astérisque required (en font-size: 1.2em) gonflaient de quelques pixels, corrigé par un line-height: 1.25rem explicite.
Impact des utilitaires dépréciés de la v3 qui ont été supprimés
Les classes retirées ne génèrent plus rien : pas d’erreur, la classe est simplement absente du CSS final. Deux régressions typiques rencontrées :
- l’overlay de nos modales
bg-black bg-opacity-25s’affichait noir opaque (bg-opacity-25supprimé) →bg-black/25; - le liseré
ring-black ring-opacity-5de nos menus devenait un ring noir plein →ring-black/5.
Le modificateur /opacité (bg-black/25) existait déjà en v3, mais sa mécanique interne change en v4 : il ne lit
plus une variable --tw-bg-opacity figée au build, il résout la couleur avec color-mix(in oklab, black 25%, transparent). C’est ce qui permet de passer currentColor en argument de /opacité (border-current/50 par
exemple) : color-mix() sait combiner n’importe quelle couleur, y compris une couleur dynamique comme
currentColor vue plus haut sur border/ring, ce qu’une variable RGBA figée au build ne pouvait pas faire.
Autres suppressions croisées dans le code : flex-shrink-0 → shrink-0, flex-grow → grow,
placeholder-gray-400 → placeholder:text-gray-400, et la palette blue-gray qui n’existe plus (→ gray).
Deux autres changements de syntaxe à surveiller :
outline-nonechange de sens : en v4 il poseoutline-style: none(invisible mais pas de contour transparent). Pour l’équivalent accessible v3 (anneau transparent pour les screen readers), utiliseroutline-hidden.- Les variables CSS en valeur arbitraire changent de délimiteur :
bg-[--brand-color]→bg-(--brand-color)(parenthèses au lieu de crochets).
Liste complète dans le guide de migration.
Autres changements ponctuels
- Le plombage interne des dégradés a changé (
--tw-gradient-position) : nos surcharges manuelles de--tw-gradient-from/tone fonctionnaient plus → à remplacer parbackground-image: linear-gradient(...)en CSS natif. - Les classes de dégradé sont renommées :
bg-gradient-to-rdevientbg-linear-to-r(idem pour-to-b,-to-l, etc.), au profit d’une API unifiée qui couvre aussi les dégradés coniques (bg-conic) et radiaux (bg-radial). Contrairement au renommageshadow/rounded, l’ancienne classe ne compile plus du tout : l’outil de migration officielnpx @tailwindcss/upgradeautomatise ce renommage, mais nous l’avons vérifié manuellement écran par écran sur nos dégradés de boutons et de bandeaux. - Le Preflight évolue : couleur des placeholders,
cursor: defaultsur les boutons. - L’ordre de stacking des variants change de droite-gauche (v3) à gauche-droite (v4) :
first:*:pt-0(v3) devient*:first:pt-0(v4). Aucun plantage à la compile, juste un rendu incorrect si ce pattern est utilisé. transition-transformcouvre désormais aussi les propriétéstranslate,scaleetrotate, dissociées detransformen custom properties séparées. Nos boutons animés entransition-[transform,opacity]ne transitaient plus leurhover:scale-105: il a fallu lister explicitementtransition-[transform,translate,scale,rotate,opacity], une régression que ni l’outil de migration ni le guide officiel ne signalent.
Nettoyage des dépendances
- Nous avons retiré
@tailwindcss/forms(une dépendance de moins) : cette librairie n’étant plus mentionnée dans la documentation de la v4 et nous avons remarqué que nous avions très peu d’impact grâce à nos composants internes. - Suppression des librairies
postcss,sassetbabelau profit de la gestion interne preprocessor du CSS natif de Tailwind v4.
4. Déclarer les variables de thème : bundle central, config projet
C’est là que la v4 change la donne pour un design system multi-projets.
En v3, le mapping des couleurs vivait dans le tailwind.config.js de chaque projet.
En v4, le thème est un fichier CSS, donc publiable par le bundle et importable comme n’importe quel style.
Le bundle publie le thème
Le design system expose un theme.css : les utilitaires (bg-primary, text-danger…) y sont définis une seule
fois, branchés sur des variables runtime que chaque projet fournit.
/* vendor/smartbooster/platform-core-bundle/assets/styles/theme.css */
@import "tailwindcss/theme.css" layer(theme) theme(static);
@theme inline static {
--color-primary-lighter: var(--primary-lighter);
--color-primary: var(--primary-default);
--color-primary-dark: var(--primary-dark);
--color-danger: var(--danger-color);
--color-success: var(--success-color);
/* ... */
}
Les deux options du bloc @theme sont importantes :
inline: la classe générée pointe directement sur la variable runtime (.bg-primary { background-color: var(--primary-default) }) au lieu d’une variable intermédiaire figée sur:root. C’est ce qui rend possibles toutes les surcharges scopées de la section suivante.static: les variables sont émises même si aucune classe ne les utilise, pour le code qui les lit au runtime (getComputedStyleen JS ou CSS legacy).
Réduire le thème par défaut
Le thème par défaut de Tailwind embarque des centaines de couleurs, espacements et tailles de police que nos 80+ composants n’utilisent jamais. Le publier tel quel depuis le bundle gonflerait inutilement le CSS de chaque projet consommateur.
La directive @theme accepte initial pour vider une famille entière de variables avant de ne redéclarer que
celles réellement utilisées :
@theme {
--color-*: initial; /* on retire toute la palette de couleurs par défaut de Tailwind */
--font-*: initial; /* idem pour les familles de polices par défaut */
--color-primary-lighter: var(--primary-lighter);
--color-primary: var(--primary-default);
/* ... uniquement les tokens réellement consommés par nos composants */
}
Ce nettoyage réduit mécaniquement le CSS généré : plus la moindre variable de couleur, de police ou d’espacement non utilisée n’est émise, ni par le bundle, ni par les projets qui l’importent.
Le projet fournit les valeurs, par interface
Chaque interface a sa propre entrée de build (admin/main.js, app/main.js), donc son propre CSS de sortie et
son propre fichier de tokens. Le projet ne déclare que des valeurs :
/* assets/admin/styles/base.css : interface back-office */
:root {
--primary-lighter: var(--color-blue-200);
--primary-default: var(--color-blue-600);
--primary-dark: var(--color-blue-700);
--danger-color: var(--color-red-600);
/* ... */
}
/* assets/app/styles/base.css : interface espace client */
:root {
--primary-lighter: var(--color-emerald-200);
--primary-default: var(--color-emerald-600);
--primary-dark: var(--color-emerald-700);
/* ... */
}
Chaque main.css d’interface assemble le tout :
/* assets/admin/styles/main.css */
@import "../../../vendor/smartbooster/platform-core-bundle/assets/styles/base.css";
@import "../../../vendor/smartbooster/platform-core-bundle/assets/styles/theme.css";
@import "base.css";
@import "tailwindcss";
Résultat : les espaces de noms sont disjoints, le bundle possède les --color-* (namespace Tailwind), le projet
possède les tokens runtime (--primary-*, --danger-color…).
Aucune bataille de cascade, et l’ordre des imports n’a pas d’incidence.
| v3 | v4 | |
|---|---|---|
| Mapping couleurs → utilitaires | recopié dans le tailwind.config.js de chaque projet | une fois, dans le theme.css du bundle |
| Valeurs du thème | _base.scss projet | base.css par interface |
| Safelist du Design Système | recopiée par projet | publiable par le bundle (@source inline dans un CSS importé) |
| Changement de thème | rebuild + config | redéfinir des variables CSS, à n’importe quel scope |
5. Surcharger le thème : par interface, par zone, par composant
Grâce à @theme inline, les classes résolvent la variable au point d’usage.
Toute redéclaration de token sur un ancêtre est donc prise en compte, sans toucher au bloc @theme.
Par interface
C’est le cas nominal vu ci-dessus : deux entrées de build, deux base.css, deux rendus, mais un seul et même design system.
Par zone, avec un attribut data-theme
Pour faire cohabiter deux thèmes dans la même page (aperçu d’un thème client dans le back-office, dark mode, section white-label) :
[data-theme="espace-client"] {
--primary-default: var(--color-emerald-600);
--primary-dark: var(--color-emerald-700);
}
<section data-theme="espace-client">
<!-- tous les bg-primary, text-primary, boutons du DS… rendent en emerald ici -->
</section>
En v3 (ou en v4 sans inline), cette surcharge serait silencieusement ignorée : la variable intermédiaire est
résolue une fois pour toutes sur :root, et les descendants héritent de la valeur figée.
Le sélecteur [data-theme="espace-client"] reste du CSS brut : il ne sert qu’à surcharger des variables, pas à
conditionner des classes utilitaires. Si le besoin évolue vers des styles Tailwind conditionnés par la zone
(espace-client:bg-primary par exemple), @custom-variant formalise le même sélecteur en variant réutilisable
partout dans le HTML :
@custom-variant espace-client (&:where([data-theme="espace-client"], [data-theme="espace-client"] *));
<div class="espace-client:rounded-full">...</div>
Nous n’en avons pas eu besoin sur ce bundle puisque la surcharge de variables suffit à tout notre theming, mais c’est la voie à prendre le jour où une zone a besoin d’un style structurel différent, pas seulement d’une couleur.
Par composant, via des variables sémantiques
Le même mécanisme descend au niveau composant.
Nos composants ne consomment pas les tokens bruts mais des variables sémantiques déclarées dans le thème :
:root {
/* base.css : les variables sémantiques dérivent des tokens */
--primary-background-color: var(--primary-default);
--primary-hover-background-color: var(--primary-dark);
--sidebar-background-color: #172554;
--sidebar-text-color: var(--primary-lighter);
}
Si on prend l’exemple du composant UiButton qui consomme uniquement les variables sémantiques, jamais les tokens bruts :
/* UiButton, style simplifié */
.btn {
background-color: var(--primary-background-color);
}
.btn:hover {
background-color: var(--primary-hover-background-color);
}
Créer une variante ne demande alors ni nouvelle classe utilitaire ni surcharge de spécificité, on redéclare la variable au scope du composant :
/* variante CTA du bouton : seul le token local change */
.btn-cta {
--primary-background-color: var(--color-orange-600);
--primary-hover-background-color: var(--color-orange-700);
}
<UiButton class="btn-cta">Souscrire</UiButton>
C’est la même règle à trois étages : le bundle définit le mapping, l’interface définit les valeurs, chaque scope (zone, composant) peut les redéfinir localement.
Note Vue SFC : pour utiliser
@applydans un<style scoped>sans que Tailwind reduplique tout le CSS, utiliser@referenceà la place d’un@import:<style scoped> @reference "../../styles/theme.css"; .my-component { @apply text-primary font-medium; } </style>
@referenceimporte uniquement les définitions (thème, utilitaires) sans émettre de CSS supplémentaire dans le bundle.
Tailwind variants : pourquoi nous ne les utilisons pas
Une autre approche existe pour gérer des variantes de composant : la librairie tailwind-variants
(ou son équivalent class-variance-authority), qui propose une fonction tv() centralisant les classes par
variante dans un fichier de configuration JS.
Reprise du même UiButton, avec une variante type valant primary ou cta :
// UiButton.variants.js
import { tv } from "tailwind-variants";
export const button = tv({
base: "rounded-md px-4 py-2 font-medium transition-colors",
variants: {
type: {
primary: "bg-primary text-white hover:bg-primary-dark",
cta: "bg-orange-600 text-white hover:bg-orange-700",
},
},
defaultVariants: {
type: "primary",
},
});
<UiButton type="cta">Souscrire</UiButton>
Note : chez SmartBooster, nous n’utilisons pas les Tailwind variants sur ce design system. Cette approche impose une configuration JavaScript (le fichier
*.variants.jset sa fonctiontv()) qui va à l’encontre de la logique CSS-first de la v4 : le thème et le mapping vivent en CSS, les variantes se retrouveraient elles seules pilotées depuis du JS. L’approche par variables CSS sémantiques vue plus haut (--primary-background-color) reste notre standard : une seule surcharge de token en CSS, sans dépendance ni fichier de configuration séparé.
6. Responsive et media queries
Même logique de centralisation pour le responsive : en v3, les breakpoints étaient du JS (theme.screens) recopié
par projet ; en v4, ce sont des variables de thème du namespace --breakpoint-*, qui pilotent à la fois les
variants (sm:, lg:…) et les utilitaires dérivés.
/* v3 : tailwind.config.js, par projet */
theme: {
screens: { sm: '640px', md: '768px', lg: '1024px', xl: '1280px' },
}
/* v4 : dans le theme.css du bundle, le standard DS hérité par tous les projets */
@theme {
--breakpoint-xs: 30rem; /* breakpoint additionnel du DS */
--breakpoint-2xl: 100rem; /* élargi pour les écrans métier */
}
/* v4 : dans le main.css d'un projet, override ponctuel */
@theme {
--breakpoint-lg: 64rem;
}
Points complémentaires :
- Breakpoints one-shot : plus besoin de toucher à la config pour un cas isolé. On écrit la valeur directement dans le template avec des valeurs arbitraires :
<!-- s'active à partir de 900px -->
<div class="min-[900px]:flex">…</div>
<!-- masqué en dessous du breakpoint lg défini dans le thème -->
<nav class="max-lg:hidden">…</nav>
- Container queries natives : en
v3, les container queries nécessitaient le plugin
@tailwindcss/container-queries. En v4, elles sont intégrées : on marque un conteneur avec@container, et ses enfants peuvent utiliser les variants@sm:,@lg:, etc., le breakpoint est alors relatif à la taille du conteneur, pas du viewport.
<div class="@container">
<div class="@sm:flex @lg:grid">…</div>
</div>
Sur un bundle partagé, plusieurs conteneurs peuvent s’imbriquer (une card dans une sidebar, elle-même dans une
grille) : nommer le conteneur avec @container/nom cible explicitement le bon niveau au lieu du plus proche.
Un variant @md: seul est déjà une largeur minimale ; combiné à @max-*, il définit une plage, utile pour une
largeur intermédiaire propre à un composant du design system :
<div class="@container/sidebar">
<div class="@container/card">
<!-- @lg/card:flex cible le conteneur "card" le plus proche, pas "sidebar" -->
<div class="@lg/card:flex @md/card:@max-xl/card:gap-6">…</div>
</div>
</div>
7. Profiter des nouveautés depuis la migration
La migration décrite plus haut portait sur la base posée par la v4.0. Depuis, les releases mineures v4.1, v4.2 et v4.3 ont ajouté des utilitaires qu’il vaut la peine d’adopter maintenant que le socle CSS-first est en place, plutôt que de les rattraper un par un au fil des besoins :
-
Dégradation mieux gérée sur les anciens navigateurs (v4.1) : la v4.0 rendait mal certaines couleurs et ombres sur les navigateurs juste en dessous du socle Safari 16.4 évoqué en partie 1 (couleurs
oklch, propriétés dépendant de@property). La v4.1 a ajouté des fallbacks au niveau du framework : le rendu reste correct même sur d’anciens Safari ou Firefox, avec au pire une nuance de couleur légèrement différente. Le point de vigilance du début de l’article est donc moins bloquant qu’au lancement de la v4. -
starting:pour les transitions d’apparition : basé sur@starting-style, ce variant anime l’apparition d’un élément (popover, menu déroulant, modale) sans JavaScript. Utile pour nos composants d’interface qui géraient jusque-là ces transitions à la main dansUiButton,UiModalet consorts. Le support navigateur suit celui évoqué en partie 1 pour@starting-style(Safari 17.5, Chrome 117), donc à vérifier au cas par cas. -
not-*, le vrai:not()CSS : remplace les contournements en@applynégatif ou en classes utilitaires dupliquées qu’on utilisait pour exclure un état précis. -
@source notet@source inline()étendu (v4.1) :@source not "chemin"exclut explicitement un dossier de la détection automatique, utile pour ignorer unvendor/tiers volumineux sans y aller au glob. La syntaxe@source inline()accepte désormais des plages numériques (bg-red-{100..900..100}), ce qui simplifie encore la safelist du bundle par rapport à l’énumération manuelle qu’on utilise en partie 2. -
Nouvelles utilitaires de scrollbar (v4.3) :
scrollbar-thumb-*etscrollbar-track-*permettent de teinter la scrollbar avec nos tokens de marque, cohérent avec l’architecture de theming par interface décrite en partie 5. -
@variantempilés et composés (v4.3) : en CSS, deux syntaxes couvrent respectivement les combinaisons et les unions de variants, utile pour les composants qui s’appuient sur@applycomme.btn-ctaen partie 5 :.btn-cta { /* combinaison : hover ET focus */ @variant hover:focus { background-color: var(--color-orange-700); } /* union : hover OU focus */ @variant hover, focus { outline: 2px solid var(--color-orange-500); } } -
Valeurs par défaut pour les utilitaires dynamiques (v4.3) :
--value(integer, --default(4))dans une définition@utilitypermet à la classe sans suffixe (tab) de retomber sur une valeur par défaut, tout en gardanttab-2fonctionnel. Un complément direct à l’exemple@utility tab-4de la partie 2. -
API de dégradés élargie : en complément du renommage
bg-linear-*vu en partie 3, la v4.0 a ajoutébg-conic-*,bg-radial-*et des modificateurs d’interpolation (bg-linear-to-r/oklch) pour des dégradés plus vifs sans CSS custom.
Conclusion
La partie mécanique de la migration (renommages, utilitaires supprimés, config CSS-first) se traite en quelques heures avec le guide officiel et une passe de recette visuelle. Les vraies régressions sont silencieuses (opacités, ombres, bordures), c’est l’écran qui les révèle, pas le build.
Le vrai gain est architectural : le thème étant devenu du CSS, notre design system publie enfin sa configuration (mapping d’utilitaires, safelist, breakpoints) comme il publie ses composants. Les projets ne possèdent plus que leurs valeurs, par interface, par zone, par composant.
Liens utiles
- Notre page technologie Tailwind CSS
- Guide de migration officiel Tailwind v4
- Theme variables (
@theme, optionsinline/static) - Détection des sources et safelist (
@source,@source inline) - Responsive design et breakpoints
- Fonctions et directives v4 (
@plugin,@custom-variant,@reference) - Preflight (styles de base)