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
variantprop, 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.
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
useVariantMergeris the component key, the one that will be used in the configuration variantPropscontains the props after merging: read your business props here, notpropsdirectly- 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 - list of components
- Themes - light and dark mode, CSS variables
- Provider Configuration
- Components and Pages