J’ai testé Tailwind CSS…

Paris 11e, un vendredi, 10h42, pause café :

Greg (développeur Back-End) : Tu n’as jamais testé Tailwind CSS ?
Allô, on est en 2021, même ma nièce de 2 ans s’y est mise !

Moi (développeur Front-End) : Mouais… c’est juste un générateur de classes utilitaires… mais ok, si tu insistes, je vais tester ça !

Rozenn (CHO) : Remplissez vos Timesheets ! 😠

Si vous êtes développeur, il est difficile de passer à côté du phénomène Tailwind, le framework CSS « Utility-First ».

N’étant pas moi-même défenseur de l’approche « Utility-First » (je vous en expliquerai les raisons dans la suite de l’article), j’ai néanmoins franchi le pas et je vous propose un rapide retour d’expérience vous permettant, je l’espère, de vous forger votre propre opinion sur cet outil. C’est parti ! 🙂

Tailwind CSS est un framework CSS « Utility-First » créé par Adam Wathan en 2017.
Depuis, Tailwind CSS n’a cessé de gagner en popularité avec la publication de deux versions majeures, la version 1 en mai 2019, et plus récemment, la version 2 en novembre 2020.

Tailwind CSS est donc un framework récent comparé aux autres frameworks CSS historiques tels que Bootstrap ou Foundation (2011–2012).

L’approche « Utility-First »

Tailwind CSS se démarque de ses aînés par son approche « Utility-First », à contrario d’une approche plus traditionnelle, par composant d’interface (approche sémantique).

Pour rappel, Tailwind CSS n’est pas le premier framework à proposer ce type d’approche :
Atomic CSS (ACSS), développé par Yahoo, se basait sur le même principe, il y a de ça quelques années (2015).

Le principe de cette approche est le suivant : à défaut d’utiliser une classe sémantique pour chaque composant d’interface, les frameworks CSS « Utility-First » vont fournir aux développeurs un ensemble de classes utilitaires. Ces classes utilitaires peuvent ensuite être combinées entre elles, afin de former des composants plus complexes.

Il est donc théoriquement possible de construire n’importe quelle interface sans écrire une seule ligne de CSS.

Ce principe s’apparente à celui des Lego : les briques (classes utilitaires) sont assemblées entre elles afin de construire votre T-Rex 🦖 (composant d’interface) exposé fièrement sur votre bureau.

L’unique différence réside dans le fait que le développeur n’aura pas accès aux instructions de montage… mais j’y reviendrai par la suite. 😉

Les avantages de l’approche « Utility-First »

Les arguments mis en avant sont généralement les suivants :

  • Il est inutile d’inventer des noms de classes ;
  • Le poids du fichier CSS reste stable tout au long du cycle de vie du projet ;
  • Les risques de régression visuelle sont réduits (modification locale en HTML versus modification globale en CSS) ;
  • Les problématiques de spécificité disparaissent (sélecteurs de classe).

Ces avantages sont bien réels mais ils sont, selon moi, également applicables à une approche par composant.

Personnellement, j’utilise depuis de nombreuses années la convention de nommage BEM inventée par Yandex, en complément de l’architecture ITCSS créée par Harry Roberts.

En pratique, la convention de nommage BEM réduit les risques de conflits liés à la spécificité et impose une rigueur quant aux nommage des composants (👋 BEMIT).

L’architecture ITCSS permet, quant à elle, d’organiser son code CSS de manière logique et structurée, afin de rendre son projet performant et maintenable tout au long du processus de développement.

Les inconvénients de l’approche « Utility-First »

Cette approche n’offre néanmoins pas que des avantages :

  • Il est indispensable d’apprendre le nom de plusieurs centaines de classes utilitaires, en plus de devoir comprendre les propriétés CSS liées à celles-ci ;
  • Les fichiers HTML deviennent rapidement illisibles (chaque balise HTML peut être associée à des dizaines de classes potentielles) ;
  • La maintenabilité du projet tend à se complexifier.

Pour approfondir le premier point, il convient de se poser cette question :

A qui s’adressent les frameworks CSS « Utility-First » ?

Les frameworks CSS « Utility-First » utilisent généralement cette accroche imparable :

Avec notre framework, vous n’aurez plus besoin d’écrire une seule ligne de CSS !

Ces frameworks CSS s’adresseraient donc principalement à des développeurs Back-End, et plus généralement, à des développeurs ne maîtrisant pas forcément le langage CSS (le fameux Greg 💛).

Et c’est, à mon sens, tout le paradoxe de l’approche « Utility-First »… en réalité, l’utilisation de ce type de framework requiert la maîtrise du langage CSS ! 🤭

Je m’explique ! Imaginons un exemple simple (désolé Greg, j’ai encore besoin de toi) :

Greg doit centrer verticalement deux blocs dans un conteneur de hauteur fluide en utilisant des classes utilitaires…

Greg : Je dois modifier la mise en page des fiches produits… J’ai testé en ajoutant la classe vertical-align-middle mais rien ne fonctionne, j’ai besoin de toi… 😭

Moi : Flexbox 😇

Greg : 😡

Greg m’avouera secrètement par la suite, qu’il ne connaissait pas les spécifications CSS du module de boîtes flexibles, et qu’il trouve ce langage bien trop compliqué et vraiment pas intéressant.

Il lui manquait de toute évidence les instructions de montage… !
(Vous vous souvenez du T-Rex ?)

Utiliser un framework CSS « Utility-First »… oui, mais pour quel type de projet ?

Ce dernier point me permet d’apporter des précisions sur la maintenabilité du projet.

La plupart du temps, les frameworks CSS « Utility-First » sont pensés pour des environnements JavaScript basés sur des composants, comme React, Vue.js, etc.

Imaginons une évolution graphique sur un composant spécifique tel qu’un bouton.

En React, l’ajout ou la suppression de classes utilitaires peut se faire directement dans le composant <Button>, lequel étant défini, idéalement, à un seul endroit.

Dans un projet Symfony, par exemple, c’est une tout autre histoire, les boutons peuvent être dupliqués dans de multiples templates. Dans cette configuration, il sera nécessaire de les modifier un par un, afin d’ajouter ou de supprimer les classes utilitaires en question. 🤪

Adam Wathan le confirme lui-même dans la documentation de son framework CSS :

But as a project grows, you’ll inevitably find yourself repeating common utility combinations to recreate the same component in many different places. This is most apparent with small components, like buttons, form elements, badges, etc.

Keeping a long list of utility classes in sync across many component instances can quickly become a real maintenance burden, …

Bref ! Après cette analyse approfondie de l’approche « Utility-First », il est maintenant grand temps d’entrer dans le vif du sujet : Tailwind CSS ! 😉

Tailwind CSS : Tour d’horizon

Les exemples d’installation qui suivent, s’appliquent, dans mon cas, à des projets Symfony, n’hésitez pas à consulter la documentation officielle de Tailwind CSS si votre environnement de développement est différent.

Nous allons installer Tailwind CSS comme plugin PostCSS.
Il est également conseillé d’installer le très populaire plugin autoprefixer, si vous ne l’avez pas déjà intégré à votre workflow.

L’installation se fait donc simplement en lançant la commande suivante :

npm install tailwindcss postcss autoprefixer -D

La deuxième étape consiste à créer (ou modifier) le fichier postcss.config.js à la racine du projet, afin d’y ajouter nos deux nouveaux plugins, tailwindcss et autoprefixer :

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}

Et c’est tout ! 🙂

Un fichier de configuration pour les gouverner tous

L’étape suivante consiste à définir les variables globales spécifiques au projet, telles que la palette de couleurs, la typographie, les valeurs d’espacement et bien d’autres, à l’aide d’un fichier JavaScript nommé tailwind.config.js et placé, une fois de plus, à la racine du projet.

Ces variables définissent notre thème :

module.exports = {
  colors: {
    primary: '#f7d325',
    secondary: '#ff2951',
    gray: {
      900: '#2b2b2a', 
    }
  },
  spacing: {
    xs : '0.8rem',
    sm: '1.6rem',
    md: '2.4rem',
    lg: '3.2rem',
  },
  fontSize: {
    sm: '1rem',
    md: '1.25rem',
    lg: '1.5rem',
  },
  fontWeight: {
    normal: 400,
    medium: 500,
    bold: 700,
  },
  borderRadius: {
    sm: '1.6rem',
    md: '2.4rem',
  },
  boxShadow: {
    md: '0 0 0.4rem rgba(0, 0, 0, 0.25)',
  }
}

Il est à noter que ce fichier est facultatif, Tailwind CSS fournit par défaut son propre thème.

Nous sommes maintenant prêts à créer nos premiers composants ! 🥳

Tailwind CSS nous propose deux manières de procéder :

  • La méthode conventionnelle, via l’approche « Utility-First » ;
  • La méthode sémantique, via l’approche par composant CSS.

Commençons par l’approche « Utility-First » (recommandée par Tailwind CSS) !

Notre premier composant via l’approche « Utility-First »

À l’aide de la documentation de Tailwind CSS, implémentons un simple bouton :

<button type="button" class="px-sm py-xs bg-primary font-bold text-md text-gray-900 rounded-sm shadow-md">
  Je suis un bouton
</button>

Les suffixes ajoutés aux classes utilitaires (-sm, -xs, -primary, etc.) correspondent aux clés renseignées dans le fichier de configuration tailwind.config.js.

Il sera maintenant nécessaire de réitérer cette étape pour l’ensemble des composants du projet.

Et voilà, à ce stade vous avez saisi le concept de l’approche « Utility-First » et par la même occasion, 80% du fonctionnement de Tailwind CSS ! 🎉

Je ne ferai même pas de remarque sur la nomenclature des classes utilitaires font-bold, text-md et text-gray-900… bon d’accord, puisque vous insistez :

  • La classe utilitaire font-{bold} est associée à la propriété CSS font-weight ;
  • La classe utilitaire text-{md} est associée à la propriété CSS font-size ;
  • La classe utilitaire text-{gray-900} est associée à la propriété CSS color.

Je vous donne cinq minutes pour trouver une cohérence dans ces noms de classes. 🤓

Et vous vous en doutez bien, j’ai évidemment testé la configuration suivante (ne faites pas ça chez vous) :

module.exports = {
  colors: {
    primary: '#f7d325',
  },
  fontSize: {
    primary: '1.25rem',
  }
}

Tailwind CSS va générer ces deux sélecteurs de classe (j’ai simplifié pour l’exemple) :

.text-primary {
  font-size: 1.25rem;
}

.text-primary {
  color: #f7d325;
}

Dommage ! 😬

Notre premier composant via la méthode sémantique

Tailwind CSS, depuis sa version 2, offre la possibilité d’extraire des classes utilitaires dans un composant CSS, à l’aide de la directive @apply.

La directive @apply

Les classes utilitaires à extraire sont identiques à celles de la version HTML :

.btn {
  @apply px-sm py-xs bg-primary font-bold text-md text-gray-900 rounded-sm shadow-md;
}

Il est donc théoriquement possible d’utiliser Tailwind CSS via l’approche traditionnelle par composant CSS, ce qui correspond mieux à mes besoins pour les raisons développées dans la première partie de l’article ! 😀

Je ne suis néanmoins pas entièrement satisfait par cette écriture pour deux raisons principales :

  • Le manque évident de lisibilité ;
  • La syntaxe spécifique à Tailwind CSS (non-standard).

La fonction theme()

Tailwind CSS propose une alternative à la directive @apply, la fonction theme().

Le principe de cette fonction est de renseigner une clé issue du fichier de configuration, afin d’accéder à sa valeur associée :

.btn {
  padding: theme('spacing.xs') theme('spacing.sm');
  background-color: theme('colors.primary');
  font-weight: theme('fontWeight.bold');
  font-size: theme('fontSize.md');
  color: theme('colors.gray.900');
  border-radius: theme('borderRadius.sm');
  box-shadow: theme('boxShadow.md');
}

À ce moment-là, je prends conscience que je suis en train d’écrire du CSS (du moins, on s’en approche), parce que jusqu’à présent, ce n’était pas une évidence. 😬

Cette solution me convient mieux du point de vue de la lisibilité, mais il reste toujours, selon moi, la problématique de la syntaxe non-standard.

Si seulement le langage CSS nous permettait de déclarer des variables globales dont les valeurs pourraient être utilisées dans un composant…? 🤔

Un « zeste » de propriétés CSS personnalisées 🍋

Oui… vous l’aurez compris, quand je regarde ce dernier exemple, je ne peux m’empêcher de penser aux propriétés CSS personnalisées.

Dans un monde idéal, j’aimerais en réalité que Tailwind CSS génère ce code :

:root {
  --color-primary: #f7d325;
  --color-secondary: #ff2951;
  --color-gray-900: #2b2b2a;

  --spacing-xs: 0.8rem;
  --spacing-sm: 1.6rem;
  --spacing-md: 2.4rem;
  --spacing-lg: 3.2rem;

  --font-size-sm: 1rem;
  --font-size-md: 1.25rem;
  --font-size-lg: 1.5rem;
  
  --font-weight-normal: 400;
  --font-weight-medium: 500;
  --font-weight-bold: 700;

  --border-radius-sm: 1.6rem;
  --border-radius-md: 2.4rem;

  --box-shadow-md: 0 0 0.4rem rgba(0, 0, 0, 0.25);
}

.p-xs {
  padding: var(--spacing-xs);
}

.p-sm {
  padding: var(--spacing-sm);
}

Je pourrais dans ce cas, implémenter mon bouton (et tous mes composants CSS) de façon standard :

.btn {
  padding: var(--spacing-xs) var(--spacing-sm);
  background-color: var(--color-primary);
  font-weight: var(--font-weight-bold);
  font-size: var(--font-size-md);
  color: var(--color-gray-900);
  border-radius: var(--border-radius-sm);
  box-shadow: var(--box-shadow-md);
}

Cette écriture est quand même beaucoup plus sexy, vous ne trouvez pas ? 💛

Pour l’anecdote, je ne suis pas le seul à attendre cette fonctionnalité.

Apparemment Adam Wathan en a pris note, et je croise les doigts 🤞 pour que la version 3 de Tailwind CSS propose une génération automatique des propriétés CSS personnalisées à partir du fichier de configuration, ce qui rendrait, selon moi, ce framework CSS incontournable.

Le grand détournement

En attendant, une solution existe afin de contourner ce problème.

L’idée est de récupérer la valeur des propriétés CSS personnalisées directement dans le fichier de configuration de Tailwind CSS :

module.exports = {
  colors: {
    primary: 'var(--color-primary)',
    secondary: 'var(--color-secondary)',
    gray: {
      900: 'var(--color-gray-900)',
    }
  },
  spacing: {
    xs : 'var(--spacing-xs)',
    sm: 'var(--spacing-sm)',
    md: 'var(--spacing-md)',
    lg: 'var(--spacing-lg)',
  },
  fontSize: {
    sm: 'var(--font-size-sm)',
    base: 'var(--font-size-md)',
    lg: 'var(--font-size-lg)',
  },
  fontWeight: {
    normal: 'var(--font-weight-normal)',
    medium: 'var(--font-weight-medium)',
    bold: 'var(--font-weight-bold)',
  },
  borderRadius: {
    sm: 'var(--border-radius-sm)',
    md: 'var(--border-radius-md)',
  },
  boxShadow: {
    md: 'var(--box-shadow-md)',
  }
}

Puis, de les déclarer manuellement à la racine du document (comme dans notre exemple précédent) :

:root {
  --color-primary: #f7d325;
  --color-secondary: #ff2951;
  --color-gray-900: #2b2b2a;

  --spacing-xs: 0.8rem;
  --spacing-sm: 1.6rem;
  --spacing-md: 2.4rem;
  --spacing-lg: 3.2rem;

  --font-size-sm: 1rem;
  --font-size-md: 1.25rem;
  --font-size-lg: 1.5rem;
  
  --font-weight-normal: 400;
  --font-weight-medium: 500;
  --font-weight-bold: 700;

  --border-radius-sm: 1.6rem;
  --border-radius-md: 2.4rem;

  --box-shadow-md: 0 0 0.4rem rgba(0, 0, 0, 0.25);
}

Les classes utilitaires générées par Tailwind CSS, ainsi que les composants CSS spécifiques au projet, auront donc accès aux valeurs des propriétés personnalisées déclarées à la racine du document. 🥳

Cette solution est loin d’être parfaite, mais a le mérite de fonctionner, voilà comment nous avons réussi à utiliser Tailwind CSS via une approche sémantique !

Dans le prochain épisode, on apprendra à utiliser Bootstrap via une approche « Utility-First » (je plaisante).

Bilan : Bien, mais peut mieux faire

Que l’on soit réticent, ou non, à l’approche « Utility-First », il faut reconnaître que Tailwind CSS sait faire parler de lui.

Sa force vient, sans nul doute, de son fichier de configuration écrit en JavaScript, réellement très bien pensé et modulable, lui permettant de s’adapter à n’importe quel environnement de travail.

L’écosystème JavaScript tend également à attirer de nombreux développeurs travaillant habituellement avec des solutions comme React ou Vue.js, contrairement à la plupart des autres frameworks CSS, traditionnellement basés sur des préprocesseurs CSS comme Sass.

Maintenant, j’insiste de nouveau sur le fait que Tailwind CSS s’adresse à des développeurs Front-End maîtrisant un minimum le langage CSS. Si vous êtes débutants, je vous recommande de partir sur un framework d’interface plus traditionnel pour les raisons abordées dans l’article.

Pour conclure, il manque à Tailwind CSS, selon moi, deux choses pour devenir le framework CSS incontournable :

  • La possibilité de générer automatiquement des propriétés CSS personnalisées basées sur le fichier de configuration. Cette fonctionnalité nous permettrait de créer nos propres composants d’interface, de manière standard en CSS, sans être contraint d’utiliser la directive @apply ou la fonction theme() ;
  • Une nomenclature fiable concernant les classes utilitaires, qui reste à ce jour très approximative (ce qui est un comble pour un framework CSS « Utility-First »).
    Personnellement, je préfère l’écriture plus stricte de ACSS évoqué en début d’article.

Greg : Alors, tu en as pensé quoi de Tailwind CSS ?

Moi : Je pense que je vais me mettre aux « styled-components » ! 🙃

Nos formations sur le sujet

  • Logo CSS avancé

    CSS avancé

    CSS est un langage incontournable pour tout développeur. Il permet de mettre en forme des pages web en offrant des possibilités de personnalisation de plus en plus impressionnantes.

blog comments powered by Disqus