---
source_hash: "0372c585"
title: "Component Variants"
weight: 220
---

# Component Variants

A **variant** is a named set of props that you apply to a SmartCommon component to modify its appearance without overriding it on every use. This is the mechanism that allows you to have a "rounded button" or a "compact list" throughout the application without repeating Tailwind classes.

## Anatomy of a Variant

A variant is an object whose keys describe the component's internal elements.

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

Two forms of keys, recognized by their writing:

| Form | Meaning | Example |
| --- | --- | --- |
| `<name>Props` | props of an internal HTML element | `buttonProps`, `labelProps`, `iconProps` |
| `<Name>` with uppercase | props of a SmartCommon sub-component | `Spinner`, `Tag` |

Uppercase keys are merged **recursively**: a `Button` variant can therefore control the `Spinner` it displays during loading.

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

To know the available keys on a component, look at the `mergeProps("...")` calls in its source: each corresponds to a variant key. For `Button`: `button`, `label`, `icon`, `badge` and the sub-component `Spinner`.

## Apply a Variant

The `variant` prop accepts three forms, combinable:

```
// 1. By name (native variant or declared in configuration)
<Button variant="rounded" />

// 2. As an object, directly
<Button variant={{ buttonProps: { className: "rounded-full" } }} />

// 3. As an array, applied from left to right
<Button variant={["rounded", "uppercase", { labelProps: { className: "text-xs" } }]} />
```

## Order and Merge Rules

Sources are applied in this order, the last one wins:

  - the variant of the **active theme**, if it designates one for this component
  - the entries of the `variant` prop, in the order of the array
  - the **props passed directly** to the component

The merge follows three rules depending on the property:

| Property | Rule |
| --- | --- |
| `className` | merge via `twMerge`: conflicting Tailwind classes are resolved, the last one wins |
| `style` | merge by spreading, key by key |
| any other prop | pure override, unless the new value is `undefined` |

> [!TIP]
> The `twMerge` pass is the reason why a variant can "cancel" a class from the base component: `rounded-full` properly replaces `rounded-md`, instead of ending up in conflict in the `class` attribute.

A `className` or `style` value can also be a **function**. It receives the parameters published by the component via `setParams`, which allows a style dependent on the internal state.

## Native Variants

SmartCommon currently provides five native variants, all for `Button`:

| Variant | Effect |
| --- | --- |
| `rounded` | fully rounded button, with adapted padding |
| `outlined` | white background, dark text, border; also adjusts the `Spinner` |
| `uppercase` | label in uppercase, normal weight, widened letter-spacing |
| `reverse` | reverses the order of the icon and label |
| `floatingRight` | positions the button floating, at the bottom right |

```
<Button variant="outlined">Cancel</Button>
<Button variant={["rounded", "uppercase"]}>Confirm</Button>
```

> [!NOTE]
> Other components expose a `variants/` folder in their source, but these files are still placeholders without content. For any component other than `Button`, use a custom variant.

## Custom Variants

Your application's own variants are declared in the configuration passed to the `Provider`, under `components.variants`, indexed by component name and then by variant name.

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

They are then used like native variants:

```
<Button variant="danger">Delete</Button>
<ListItem variant="compact" />
```

If a custom name uses a native name, both definitions merge.

> [!IMPORTANT]
> This mechanism was long non-functional, and the point is worth knowing if you're taking over an old project: until a recent SmartCommon fix, **no** form of variant was applied, with the sole exception of a native name passed in an array (`variant={["rounded"]}`). Neither string names, nor objects, nor themes, nor Provider configuration. If you find that a variant has no effect, first check the SmartCommon version before looking in your code.

## Themes

A theme associates, for each component, one or more variants to apply by default throughout the application.

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

The theme's variant is applied **before** the `variant` prop, which can therefore override it punctually.

> [!NOTE]
> Do not confuse `components.theme`, which designates a set of variants, with the `theme` prop of the `Provider`, which manages light, dark or automatic mode. The two are independent. See [Themes](/front/themes).

## Components that Accept Variants

More than 70 components use `useVariantMerger` and therefore accept a `variant` prop.

| Family | Components |
| --- | --- |
| Form | `Input`, `Select`, `SearchableSelect`, `Textarea`, `Checker`, `Boolean`, `RadioBar`, `Range`, `Rater`, `ColorPicker`, `Editor`, `Calendar`, `PlainCalendar`, `NumericPad`, `PinPad`, `Timer`, `Gps`, `AddressInput`, `Array`, `SignaturePad`, `PhotosUploader`, `VideosUploader`, `AudiosUploader` |
| Display | `Address`, `Array`, `Color`, `Coordinates`, `Datetime`, `Duration`, `Email`, `Files`, `Icon`, `Number`, `PhoneNumber`, `Signature`, `String`, `Tags`, `Text`, `Url` |
| Elements | `Button`, `FAB`, `Spinner`, `Tag` |
| Layout | `Page`, `Block`, `Panel`, `Popup`, `List`, `ListItem` |
| Navigation | `Navbar`, `Sidebar`, `Tabbar`, `TabbarItem`, `ChipBar`, `LowerNavbarItem`, `UpperNavbarItem` |

## Accept Variants in Your Own Component

An application component can use the same mechanism, with `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>
  );
};
```

What to remember:
- the first argument of `useVariantMerger` is the component key, the one that will be used in the configuration
- `variantProps` contains the props after merging: read your business props here, not `props` directly
- each `mergeProps("<key>", ...)` creates a hook point for variants
- an uppercase key (`mergeProps("Spinner", ...)`) designates a sub-component and merges recursively

## Pitfalls to Know

| Symptom | Cause |
| --- | --- |
| a named variant has no effect | SmartCommon version before the variant resolution fix |
| a variant works in array but not in string | same cause: it's the exact signature of the fixed bug |
| a Tailwind class is ignored | conflict resolved by `twMerge` in favor of a more priority source; check the merge order |
| a business prop is `undefined` in the component | it is read from `props` instead of `variantProps` |
| the variant applies to the wrong element | wrong key: `buttonProps` targets the element, `Button` targets a sub-component |
| a custom class does not merge | `twMerge` doesn't know it; declare it in `components.tailwindCss.mergedClass` |

## See Also
- [SmartCommon](/front/smartcommon) - list of components
- [Themes](/front/themes) - light and dark mode, CSS variables
- [Provider Configuration](/front/configuration)
- [Components and Pages](/front/composants-et-pages)
