Intégrer un Design System avec Reactjs, Nextjs, TailwindCSS et CVA

Publié le 29 janvier 2024
Baptiste Drillien avatar
Baptiste Drillien
Développeur front-end

Un Design System se doit d’être robuste, il constitue le coeur d’une application. Il est indispensable et représente les fondations du projet.

Dans cet article, nous allons voir comment intégrer proprement un Design System en faisant face aux problématiques classiques qu’on peut rencontrer en utilisant des outils modernes.

Découvrons donc CVA (Class Variance Authority), qui va nous permettre de construire ces bases, dans une stack classique mais moderne : NextJs, avec Typescript et TailwindCSS.

Pourquoi CVA ?

Nous allons utiliser l’exemple des boutons, composant indispensable du Design System, qui présente parfois une certaine complexité.

design system button example

Intégration d'un bouton dans un Design System

Pour créer un bouton facilement réutilisable, il faut prendre en compte ses différents variants, comme par exemple :

  • Ses différents intents : primary secondary ghost
  • Ses différentes tailles : sm md lg

Création du composant

Intégrer un bouton avec notre stack, sans CVA, nous oblige à créer différents objets, contenant des class tailwind, que nous utilisons selon les props. De plus, nous faisons appel à tailwind-merge pour gérer les class dynamiques.

button.tsx
1import { twMerge } from "tailwind-merge"
2
3const base = "font-medium"
4
5const intents = {
6 primary: "bg-royal-blue-500 text-jonquil-200",
7 secondary: "bg-jonquil-200 text-royal-blue-500",
8 ghost: "text-royal-blue-500",
9}
10
11const sizes = {
12 sm: "text-sm py-1.5 px-2.5 rounded",
13 md: "text-base py-2.5 px-3.5 rounded-md",
14 lg: "text-2xl py-4 px-6 rounded-xl",
15}
16
17type Props = React.ButtonHTMLAttributes<HTMLButtonElement> & {
18 intent?: keyof typeof intents
19 size?: keyof typeof sizes
20}
21
22export const Button = ({
23 intent = "primary",
24 size = "md",
25 className,
26 ...props
27}: Props) => (
28 <button
29 className={twMerge(base, intents[intent], sizes[size], className)}
30 {...props}
31 />
32)

Bien que nous ayons un composant aux apparences solides, il présente plusieurs problèmes.

Par exemple, si je souhaite utiliser mon bouton pour afficher des icons, je veux qu’il soit rond peu importe sa taille. Ça devient rapidement verbeux et compliqué.

Pour résumer :

  • il faut manuellement ajouter des props pour chacun de nos objets.
  • il faut manuellement ajouter les types qui correspondent.
  • il faut gérer certains cas plus complexes manuellement (comme les icons).
  • les composants n’ont aucune cohérence, on déclare les variants différemment à chaque fois.

C’est là qu’entre en jeu CVA.

CVA (Class Variance Authority), c'est quoi ?

CVA est une librairie créée spécifiquement pour nous faciliter la tâche lorsque l’on intègre des composants de notre Design System.

“ Creating variants with the "traditional" CSS approach can become an arduous task; manually matching classes to props and manually adding types. ”
cva.style

Plus de redondance, une meilleure cohérence entre nos composants, une meilleure lisibilité de l’intégration et des variants différents. CVA apporte une robustesse qui devient indispensable à la création de nos composants.

Création d’un composant avec CVA

Reprenons l’exemple de notre bouton. Nous souhaitons un bouton avec 3 intents, 3 tailles différentes, qui puisse facilement être utilisé sous forme de bouton d’icon ou de bouton traditionnel.

Après avoir installé la librairie et lu la documentation, commençons l’intégration.

D’abord les intents et les tailles

button.tsx
1import { twMerge } from "tailwind-merge"
2import { cva, type VariantProps } from "class-variance-authority"
3
4const button = cva(["font-medium"], {
5 variants: {
6 intent: {
7 primary: ["bg-royal-blue-500", "text-jonquil-200"],
8 secondary: ["bg-jonquil-200", "text-royal-blue-500"],
9 ghost: ["text-royal-blue-500"],
10 },
11 size: {
12 sm: ["text-sm", "py-1.5", "px-2.5", "rounded"],
13 md: ["text-base", "py-2.5", "px-3.5", "rounded-md"],
14 lg: ["text-2xl", "py-4", "px-6", "rounded-xl"],
15 },
16 },
17 defaultVariants: {
18 intent: "primary",
19 size: "md",
20 },
21})
22
23type Props = React.ButtonHTMLAttributes<HTMLButtonElement> &
24 VariantProps<typeof button>
25
26export const Button = ({ className, intent, size, ...props }: Props) => (
27 <button
28 className={twMerge(button({ intent, size, className }))}
29 {...props}
30 />
31)
32

Mon composant est facilement lisible et modifiable. Il est totalement typé et en suivant la documentation de CVA, je bénéficie de l’auto-complétion de TailwindCSS en ajoutant du style dans mes objets.

Je souhaite maintenant utiliser mon composant avec des icons, sans texte, qui sera rond peu importe la taille.

Hexa web
Besoin d’une équipe de développeurs front-end ?

Avec des icons

button.tsx
1import { twMerge } from "tailwind-merge"
2import { cva, type VariantProps } from "class-variance-authority"
3
4const button = cva(["font-medium"], {
5 variants: {
6 intent: {
7 primary: ["bg-royal-blue-500", "text-jonquil-200"],
8 secondary: ["bg-jonquil-200", "text-royal-blue-500"],
9 ghost: ["text-royal-blue-500"],
10 },
11 size: {
12 sm: ["text-sm", "py-1.5", "px-2.5", "rounded"],
13 md: ["text-base", "py-2.5", "px-3.5", "rounded-md"],
14 lg: ["text-2xl", "py-4", "px-6", "rounded-xl"],
15 },
16 btnType: {
17 button: "",
18 icon: ["p-0", "rounded-full"],
19 },
20 },
21 compoundVariants: [
22 { btnType: "icon", size: "sm", class: "h-10 w-10" },
23 { btnType: "icon", size: "md", class: "h-11 w-11" },
24 { btnType: "icon", size: "lg", class: "h-12 w-12" },
25 ],
26 defaultVariants: {
27 btnType: "button",
28 intent: "primary",
29 size: "md",
30 },
31})
32
33type Props = React.ButtonHTMLAttributes<HTMLButtonElement> &
34 VariantProps<typeof button>
35
36export const Button = ({
37 btnType,
38 className,
39 intent,
40 size,
41 ...props
42}: Props) => (
43 <button
44 className={twMerge(button({ btnType, className, intent, size }))}
45 {...props}
46 />
47)

En quelques lignes je peux modifier le comportement de mon composant pour facilement changer ses propriétés lorsque certaines conditions sont réunies.

Ici, j’ai ajouté un nouveau variant à mon composant :

btnType: {
button: "",
icon: ["p-0", "rounded-full"],
},

Et j'ai défini que lorsque :

btnType === "icon" && size === "sm"

Alors, j’ajoute une nouvelle class :

class: "h-10 w-10"

Avec cette simple ligne, j’ai une logique toute faite, typée et explicite pour modifier le style de mon composant. Je peux faire plein de combinaisons, et intégrer n’importe quel type de design system très facilement.

Conclusion

CVA est une librairie très récente et encore peu connue. Avec l’émergence de NextJS qui devient aujourd’hui incontournable et TailwindCSS étant proposé par défaut dans chaque projet, on rencontre très souvent les problématiques auxquelles CVA répond.

CVA apporte des solutions qui semblent évidentes mais qui n’existaient pas auparavant. Il est possible de l'utiliser avec TailwindCSS mais également avec des modules CSS classiques.

Pour aller plus loin : il est important de documenter ses composants au sein du Design System, par exemple avec StorybookJs. Il est également possible de créer un package spécifique pour son Design System afin de l’utiliser dans différents projets.