Variants de composants

Un variant est un jeu de props nommé, que l'on applique à un composant SmartCommon pour en modifier l'apparence sans le surcharger à chaque usage. C'est le mécanisme qui permet d'avoir un "bouton arrondi" ou une "liste compacte" dans toute l'application sans répéter les classes Tailwind.

Anatomie d'un variant

Un variant est un objet dont les clés décrivent les éléments internes du composant.

const rounded = {
  buttonProps: {
    className: "p-app-base rounded-full",
  },
};

Deux formes de clés, reconnues à leur écriture :

Forme Signification Exemple
<nom>Props props d'un élément HTML interne buttonProps, labelProps, iconProps
<Nom> avec majuscule props d'un sous-composant SmartCommon Spinner, Tag

Les clés en majuscule sont fusionnées récursivement : un variant de Button peut donc piloter le Spinner qu'il affiche pendant un chargement.

const outlined = {
  buttonProps: {
    className: "text-gray-800 bg-white border",
  },
  Spinner: {
    spinnerProps: {
      className: "border-primary border-l-secondary",
    },
  },
};

Pour connaître les clés disponibles sur un composant, regardez les appels à mergeProps("...") dans sa source : chacun correspond à une clé de variant. Pour Button : button, label, icon, badge et le sous-composant Spinner.

Appliquer un variant

La prop variant accepte trois formes, combinables :

// 1. Par son nom (variant natif ou declare dans la configuration)
<Button variant="rounded" />

// 2. En objet, directement
<Button variant={{ buttonProps: { className: "rounded-full" } }} />

// 3. En tableau, applique de gauche a droite
<Button variant={["rounded", "uppercase", { labelProps: { className: "text-xs" } }]} />

Ordre et règles de fusion

Les sources sont appliquées dans cet ordre, la dernière gagne :

  • le variant du thème actif, s'il en désigne un pour ce composant
  • les entrées de la prop variant, dans l'ordre du tableau
  • les props passées directement au composant

La fusion suit trois règles selon la propriété :

Propriété Règle
className fusion par twMerge : les classes Tailwind conflictuelles sont résolues, la dernière l'emporte
style fusion par étalement, clé par clé
toute autre prop écrasement pur, sauf si la nouvelle valeur est undefined

Astuce

Le passage par twMerge est la raison pour laquelle un variant peut "annuler" une classe du composant de base : rounded-full remplace proprement rounded-md, au lieu de se retrouver en concurrence dans l'attribut class.

Une valeur de className ou de style peut aussi être une fonction. Elle reçoit les paramètres publiés par le composant via setParams, ce qui permet un style dépendant de l'état interne.

Variants natifs

SmartCommon livre aujourd'hui cinq variants natifs, tous sur Button :

Variant Effet
rounded bouton entièrement arrondi, avec un padding adapté
outlined fond blanc, texte sombre, bordure ; ajuste aussi le Spinner
uppercase libellé en capitales, graisse normale, interlettrage élargi
reverse inverse l'ordre de l'icône et du libellé
floatingRight positionne le bouton en flottant, en bas à droite
<Button variant="outlined">Annuler</Button>
<Button variant={["rounded", "uppercase"]}>Valider</Button>

Note

Les autres composants exposent un dossier variants/ dans leur source, mais ces fichiers sont encore des emplacements réservés, sans contenu. Pour tout composant autre que Button, passez par un variant personnalisé.

Variants personnalisés

Les variants propres à votre application se déclarent dans la configuration passée au Provider, sous components.variants, indexés par nom de composant puis par nom de variant.

// src/appConfig.js
export const appConfig = {
  components: {
    variants: {
      Button: {
        danger: {
          buttonProps: {
            className: "bg-red-600 text-white hover:bg-red-700",
          },
        },
        ghost: {
          buttonProps: {
            className: "bg-transparent border-none shadow-none",
          },
        },
      },
      ListItem: {
        compact: {
          itemProps: { className: "py-1 text-sm" },
        },
      },
    },
  },
};
// src/main.jsx
<Provider config={appConfig}>
  <Router />
</Provider>

Ils s'utilisent ensuite comme les variants natifs :

<Button variant="danger">Supprimer</Button>
<ListItem variant="compact" />

Si un nom personnalisé reprend un nom natif, les deux définitions fusionnent.

Important

Ce mécanisme a longtemps été inopérant, et le point mérite d'être connu si vous reprenez un projet ancien : jusqu'à un correctif récent de SmartCommon, aucune forme de variant ne s'appliquait, à la seule exception d'un nom natif passé dans un tableau (variant={["rounded"]}). Ni les noms en chaîne, ni les objets, ni les thèmes, ni la configuration du Provider. Si vous constatez qu'un variant est sans effet, vérifiez d'abord la version de SmartCommon avant de chercher dans votre code.

Thèmes

Un thème associe, pour chaque composant, un ou plusieurs variants à appliquer par défaut dans toute l'application.

components: {
  theme: "compact",
  themes: {
    compact: {
      Button: "rounded",
      ListItem: ["compact", "borderless"],
    },
  },
}

Le variant du thème est appliqué avant la prop variant, qui peut donc le surcharger ponctuellement.

Note

Ne pas confondre components.theme, qui désigne un jeu de variants, avec la prop theme du Provider, qui gère le mode clair, sombre ou automatique. Les deux sont indépendants. Voir Thèmes.

Composants qui acceptent des variants

Plus de 70 composants passent par useVariantMerger et acceptent donc une prop variant.

Famille Composants
Formulaire Input, Select, SearchableSelect, Textarea, Checker, Boolean, RadioBar, Range, Rater, ColorPicker, Editor, Calendar, PlainCalendar, NumericPad, PinPad, Timer, Gps, AddressInput, Array, SignaturePad, PhotosUploader, VideosUploader, AudiosUploader
Affichage Address, Array, Color, Coordinates, Datetime, Duration, Email, Files, Icon, Number, PhoneNumber, Signature, String, Tags, Text, Url
Éléments Button, FAB, Spinner, Tag
Mise en page Page, Block, Panel, Popup, List, ListItem
Navigation Navbar, Sidebar, Tabbar, TabbarItem, ChipBar, LowerNavbarItem, UpperNavbarItem

Accepter des variants dans son propre composant

Un composant applicatif peut utiliser le même mécanisme, avec useVariantMerger.

import { useVariantMerger } from '@cap-rel/smartcommon';

export const MyCard = (props) => {
  const { variantProps, mergeProps, setParams } = useVariantMerger("MyCard", props);

  const { title, children } = variantProps;

  return (
    <div {...mergeProps("card", props => ({
      ...props,
      className: "rounded-lg border p-4",
    }))}>
      <h3 {...mergeProps("title", props => ({
        ...props,
        className: "font-semibold",
      }))}>
        {title}
      </h3>
      {children}
    </div>
  );
};

Ce qu'il faut retenir :

  • le premier argument de useVariantMerger est la clé du composant, celle qu'on utilisera dans la configuration
  • variantProps contient les props après fusion : lisez-y vos props métier, pas props directement
  • chaque mergeProps("<clé>", ...) crée un point d'accroche pour les variants
  • une clé en majuscule (mergeProps("Spinner", ...)) désigne un sous-composant et se fusionne récursivement

Pièges à connaître

Symptôme Cause
un variant nommé n'a aucun effet version de SmartCommon antérieure au correctif de résolution des variants
un variant fonctionne en tableau mais pas en chaîne même cause : c'est la signature exacte du défaut corrigé
une classe Tailwind est ignorée conflit résolu par twMerge en faveur d'une source plus prioritaire ; vérifiez l'ordre de fusion
une prop métier est undefined dans le composant elle est lue sur props au lieu de variantProps
le variant s'applique au mauvais élément mauvaise clé : buttonProps cible l'élément, Button cible un sous-composant
une classe personnalisée ne fusionne pas twMerge ne la connaît pas ; la déclarer dans components.tailwindCss.mergedClass

Voir aussi