J'utilise Tailwind tous les jours depuis environ 6 mois, j'ai désormais assez de recul pour pouvoir vous partager mon expérience de ce framework CSS, tant aimé par certains, tant détesté par d'autres.
Je connais Tailwind depuis ses débuts, mais je n'ai eu l’occasion que de m'en servir vraiment que récemment sur un beau projet NextJS et la conception d'un design system.
Alors c’est le moment d’en parler, en bien, mais aussi en mal, et répondre à des questions que je vois souvent passer.
Faut-il connaître le CSS pour utiliser Tailwind ?
La réponse est oui, je ne pense pas que cette question fasse débat.
Tailwind ne propose pas d’abstractions, c'est à dire qu'une classe correspond toujours (sauf quelques rares exeptions) à une seule règle CSS.
Par exemple la classe .flex
va ajouter display: flex
au build CSS, et c'est tout.
Un autre exemple : la classe .text-center
ajoutera un text-align: center
.
C'est le principe des classes utilitaires, contrairement à un Bootstrap qui lui propose des abstractions avec ses classes .btn
ou .card
qui font référence à plusieurs règles CSS.
Il faut donc connaître le CSS car il faut comprendre le CSS derrière les classes qu'on utilise.
Sauf si on copie/colle des composants Tailwind déjà faits ! 😅
Mais quand on connait le CSS, l'utilisation de Tailwind ça donne quoi ?
Si on est habitué au concept des classes utilitaires, et qu'en plus on connait le CSS, alors Tailwind ne s'apprend pas vraiment.
Le gros du boulot c’est d'apprendre comment créer sa config, et comprendre comment Tailwind génère le build CSS.
Il faut aussi avoir une réflexion sur la maintenabilité de son code, en mettant en place des conventions (et s'engager à les respecter), comme l'ordre des classes ou ne pas utiliser de classes avec des valeurs arbitraires etc..
Sinon ça devient dur à lire et à maintenir.
Par exemple, tu arrives à lire facilement ce code ? 🤯
<div class="bg-gradient-to-r from-red-500/50/[0.31] via-indigo-700 items-baseline backdrop-invert-0 md:row-start-5 sm:content-around leading-snug dark:tracking-wider placeholder-gray-50::placeholder">🤯</div>
Bon ok c'est un exemple un peu extrême, normalement on ne doit pas en arriver là, mais à aucun moment la documentation nous dit de ne pas faire ça.
Mais cette flexibilité, c'est aussi un des plus gros atout de Tailwind : on peut (presque) tout faire avec des classes utilitaires.
La force de Tailwind : assumer le 100% classes utilitaires
Assumer le 100% classes utilitaires et donc théoriquement ne plus avoir de classes custom (peut-être même plus de fichiers CSS), cela vient régler un gros problème : le nommage des classes.
Oui, c'est dur de donner des noms aux classes des éléments, et même avec de la bonne volonté on se retrouve parfois avec des .sidebar-nav-inner-bidule
.
Il faut lire cet article de Adam Wathan (le créateur de Tailwind) qui date de 2017 dans lequel il explique comment il en est arrivé là, après avoir essayé plusieurs méthodes et conventions de nommage (dont BEM), il en arrive à la conclusion que le mieux est de faire du 100% classes utilitaires (c'est sa conclusion, on peut ne pas être d'accord).
Mais selon les projets, ne pas avoir de classes custom n’est pas toujours possible, j'ai déjà travaillé sur des sites sur lesquels il fallait absolument une classe ou un id custom pour cibler un élément depuis un service externe comme un outil de gestion de tag (Google Tag Manager pour ne pas le citer).
Le HTML est illisible !
Oui, le DOM est moche à regarder, et même si on bosse par composant, un composant <Radio />
aura une liste de classes relativement longue.
On peut s'appuyer sur des packages comme clsx pour nous aider à "ranger" les classes :
import clsx from 'clsx';
export const Component = () => (
<div
className={clsx(
'block bg-white text-left text-black space-y-2 py-2 w-full',
'sm:w-1/2 sm:space-y-0',
'md:w-1/3 md:space-x-6',
'dark:bg-gray-900 dark:text-white',
)}
>
Hello
</div>
);
Même avec cette méthode, le code peut-être dur à maintenir et à lire, même si avec le temps on s'habitue.
Néanmoins le 100% classes utilitaires à un avantage considérable : pouvoir "voir" son composant juste en lisant le markup, ça c’est génial.
Pour éviter d'avoir des listes de classes trop longue, on peut utiliser la directive @apply
pour "rassembler" plusieurs classes dans une classe custom, mais c'est considéré comme une mauvaise pratique dans la documentation.
Après tout c'est normal, on abandonne le côté 100% utilitaire, puisqu'on crée une abstraction, et qu'on ajoute directement les classes au build CSS :
.btn-primary {
@apply bg-primary text-white font-bold py-2 px-4 rounded;
}
Easy to write, but hard to read
On peut lire ceci de pas mal de développeurs : "Tailwind is easy to write, but hard to read". On peut aller plus loin et dire : "Tailwind is fun and easy to write, but hard to read and maintain".
Cela reste un avis personnel, mais même après plusieurs dizaines d'heures d'utilisation, Tailwind reste fun à utiliser.
Styliser un composant sans quitter le markup et sans cette charge mentale de nommer les classes, c'est top. 🙂
Mention spéciale pour les classes qui permettent de gérer le responsive et les states (hover, focus etc.), écrire les Media Queries et les pseudo-classes en CSS c'est fastidieux à la longue, cette partie du CSS devient agréable avec Tailwind.
Mais maintenir proprement un projet Tailwind, c'est une autre histoire, c'est assez pénible de remettre le nez dans ces listes de classes longues comme le bras pour venir y faire des modifications.
Surtout si on ne respecte pas de conventions (ses propres conventions, ou celles définies en équipe), car on est vite tenté d'ajouter des classes un peu n'importe comment.
Il faut absolument comprendre comment Tailwind génère le build CSS
Dans la configuration de Tailwind (le fichier tailwind.config.js
), on définit les fichiers à observer, et Tailwind doit pouvoir trouver dans ces fichiers les noms des classes écrit entièrement.
C'est pour ça que le code ci-dessous ne fonctionnera pas :
export const Component = ({color}) => (
<div
className={`text-${color}`}
>
Hello
</div>
);
Même si dans le DOM la classe est écrite en toutes lettres, comme text-red-500
par exemple.
C'est normal, Tailwind compile au moment du build, il ne peut pas savoir que la variable color vaut "red-500".
Il faut que les classes soient écrites en toutes lettres dans le code, pour que Tailwind puisse les "voir" au moment de son scan des fichiers qui sont observés.
C'est un problème car on n'a pas forcément envie de lister toutes les possibilités de classes qu'un composant peut recevoir.
Alors il existe des solutions/alternatives à ce problème, je t'en propose 3 :
1. Utiliser clsx pour conditionner le rendu des classes
Le package clsx n'est pas seulement utile pour "ranger" les classes Tailwind, il est surtout très utile pour conditionner facilement le rendu de ces dernières.
Il faudra néanmoins écrire toutes les classes possibles, l'avantage c'est que l'on peut gérer des conditions de rendu des classes assez complexes, un exemple :
export const Component = ({theme, outline}) => (
<div
className={clsx(
{
'bg-primary text-white': theme === 'primary',
'bg-secondary text-white': theme === 'secondary',
},
{
'bg-transparent text-primary': outline && theme === 'primary',
'bg-transparent text-secondary': outline && theme === 'secondary',
},
'text-center font-bold py-2 px-4 rounded'
)}
>
Button
</div>
);
Le composant n'aura ainsi pas les mêmes classes en fonction de la valeur de la prop theme
et outline
, et comme les classes sont écrites en toutes lettres, Tailwind peut les "voir" et les inclure dans le build CSS.
Il existe d'autres outils qui permettent de faire à peu près la même chose, comme classnames par exemple, ou encore class-variance-authority.
2. Lister les classes en toutes lettres dans une <div>
cachée dans un fichier observé par Tailwind
J'ai vu cette technique plusieurs fois proposée par des developpeurs pourtant aguéris.
Ce n'est clairement pas une bonne idée et une bonne pratique, surtout si cette <div>
est présente dans le DOM.
On vient alourdir le DOM et c'est très compliqué à maintenir sur la durée, surtout avec les déclinaisons de classes (responsive, states etc.).
3. Créer une safelist des classes
C'est la solution proposée par la documentation quand cela devient trop laborieux de les écrire en toutes lettres : faire une "safelist" des classes.
L'idée c'est que Tailwind va injecter toutes les classes présentes dans cette liste, même s'il ne les trouve pas dans les fichiers observés.
La safelist est un tableau à mettre dans le fichier de config Tailwind :
module.exports = {
content: ['./src/**/*.js'],
safelist: ['bg-primary', 'bg-secondary', 'text-primary', 'text-secondary'],
theme: {},
variants: {},
plugins: [],
};
Avec le code ci-dessus, les classes bg-primary
, bg-secondary
, text-primary
, text-secondary
seront automatiquement ajoutées au build CSS, même si Tailwind ne les trouve pas dans les fichiers observés.
Si on a besoin d'utiliser ces classes sur des breakpoints spécifiques ou sur certains états (hover, focus..), alors il faudra aller plus loin et générer les classes avec des patterns :
module.exports = {
content: ['./src/**/*.js'],
safelist: [
{
pattern: /(bg|text)-(primary|secondary)/,
variants: ['lg', 'xl']
}
],
theme: {},
variants: {},
plugins: [],
};
Ici on va générer toutes les classes qui commencent par bg-
ou text-
et qui contiennent primary
ou secondary
avec les variants lg
et xl
.
Le safelisting n'est pas recommandé dans la documentation, au même titre que l'utilisation de la directive @apply
, car on inclut des classes dans le build CSS qui ne seront potentiellement jamais utilisées dans le projet.
Niveau taille du build CSS ça donne quoi ?
Le fichier de build monte vite en taille au début mais ça se stabilise à un moment.
Un exemple : la classe flex
va être utilisée plusieurs dizaines de fois, mais dans le build elle ne fera référence qu’à une seule règle CSS !
Mais il faut surtout dire merci au concept des classes utilitaires plutôt qu'à Tailwind, qui ne fait rien de magique au final.
Si on utilise bien Tailwind, le build CSS ne contiendra pas de classes zombies, c'est à dire des classes qui ne sont jamais utilisées dans le DOM ou dans le projet entier.
C'est pour ça que le safelisting et la directive @apply
ne sont pas recommandés dans la documentation, on force Tailwind à inclure des classes potentiellement zombie.
Le piège dans lequel on peut tomber : vouloir tout faire avec Tailwind
Même si la documentation essaye de nous faire un peu croire le contraire, on ne peut pas tout faire avec Tailwind.
Alors d'accord il y a les classes que l'on peut générer avec des valeurs arbitraires, mais ça devient vite la foire à la saucisse. 🌭
Les classes complexes comme lg:hover:before:bg-primary
personnellement j'ai du mal (mais c'est perso).
Les grilles (CSS Grid) complexes sont difficiles à reproduire (impossible ?) et on n'a pas accès à tous les sélecteurs CSS.
Une chose que je reproche à Tailwind : on est incité à tout faire avec, et ce n'est pas une bonne idée je trouve.
De plus, la documentation ne donne pas vraiment de guidelines, c'est à nous de mettre en place des conventions et règles à respecter.
Écrire des guidelines à respecter, déjà c'est du boulot, on est obligé de sortir des usages proposés par Tailwind, et il faut que tout le monde dans une équipe de développeurs les comprennent et les respectent.
C'est dommage de pas avoir la documentation qui fasse office de source de vérité pour les bonnes pratiques..
La bizarrerie des layers @tailwind
Une particularité pour le moins étrange est que Tailwind utilise une syntaxe custom pour les layers CSS, et c'est un problème.
Les directives Tailwind sont des faux layers qui servent juste de "portails" pour inclure des styles à l'intérieur.
Ces faux layers tu les connais si tu utilises Tailwind, ce sont les directives @tailwind base
, @tailwind components
et @tailwind utilities
.
Voici un exemple d'utilisation, ici on inclut un style dans le "layer" base
:
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Ici on ne crée pas de vrai layer CSS, on inclut juste un style dans le "layer" base */
@layer base {
h1 {
font-size: 30px;
}
}
Dans le build CSS on aura juste l'ordre qui sera respecté, les layers n'y sont absolument pas présents.
Même si la font-size: 30px
sur le <h1>
sera bien positionnée avant les styles présents dans les "layers" components
et utilities
, c'est quand même un problème pour plusieurs raisons :
-
Déjà la syntaxe, Tailwind utilise la même syntaxe CSS qui est utilisée pour faire des vrais layers CSS, j'ai cherché j'ai pas trouvé la raison de ce choix étrange. On peut créer des vrais layers CSS, mais c'est juste que les "layers" fournis par Tailwind n'en sont pas.
-
Ces layers Tailwind ne sont alors pas visibles dans les devtools du navigateur, on ne les voit pas, puisqu'ils ne sont pas de vrais layers CSS ! Il existe une astuce simple pour contourner ce problème : encapsuler ces faux layers dans de vrais layers CSS, qui eux seront bien présents dans le build CSS.
-
Cela peut causer des problèmes de spécificité. En effet, les règles CSS qui ne sont pas dans un layer prendront le dessus sur les règles qui sont dans un layer (à part si il y a un
!important
).
Si tu veux en savoir plus sur les layers CSS, tu peux trouver la documentation sur MDN.
Tailwind est-il un bon choix pour la conception d'un design system ?
Je dirais non car même si Tailwind apporte beaucoup de solutions, il amène aussi pas mal de problèmes, mais je manque d'expérience dans la conception de design system pour en parler et argumenter.
Je te conseille cet article, il est excellent : Don't use Tailwind for your Design System.
Je referai un bilan après une année entière d'utilisation, on verra comment mon avis aura évolué, mais je suis déjà assez sûr de vouloir aller voir chez d'autres projets comme PandaCSS, qui est une solution pour faire du CSS-In-JS, au build time.
Je te remercie de m'avoir lu, n'hésites pas à me donner ton avis sur Twitter !
Si tu souhaites être prévenu de la sortie des prochains articles et contenu du site, tu trouveras le formulaire d'inscription à ma newsletter un peu plus bas.
À bientôt, Seb.