---
title: "Variants de composants"
weight: 220
---

# 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` |

> [!TIP]
> 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](/front/themes).

## 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
- [SmartCommon](/front/smartcommon) - liste des composants
- [Thèmes](/front/themes) - mode clair et sombre, variables CSS
- [Configuration du Provider](/front/configuration)
- [Composants et pages](/front/composants-et-pages)
