Stylisation
Stylisation conditionnelle et composable avec des classes Tailwind.
Les bibliothèques de composants modernes ont besoin de systèmes de stylisation flexibles capables de gérer des exigences complexes sans sacrifier l'expérience développeur. La combinaison de Tailwind CSS avec une fusion intelligente des classes s'est imposée comme un modèle puissant pour construire des composants personnalisables.
Cette approche résout la tension fondamentale entre fournir des valeurs par défaut sensées et permettre une personnalisation complète — un défi qui a pesé sur les bibliothèques de composants pendant des années.
Le problème des approches de stylisation traditionnelles
Les approches CSS traditionnelles conduisent souvent à des guerres de spécificité, des conflits de style et des remplacements imprévisibles. Lorsque vous passez className="bg-blue-500" à un composant qui a déjà bg-red-500, lequel l'emporte ?
Sans gestion appropriée, les deux classes s'appliquent et le résultat dépend de nombreux facteurs — l'ordre source du CSS, la spécificité des classes, l'algorithme de fusion des classes du bundler, etc.
Fusionner les classes de manière intelligente
La bibliothèque tailwind-merge résout ce problème en comprenant la structure des classes Tailwind et en résolvant intelligemment les conflits. Lorsque deux classes ciblent la même propriété CSS, elle ne conserve que la dernière.
// Both bg-red-500 and bg-blue-500 apply - unpredictable result
<Button className="bg-blue-500" />
// Renders: className="bg-red-500 bg-blue-500"import { twMerge } from 'tailwind-merge';
// bg-blue-500 wins as it comes last
const className = twMerge('bg-red-500', 'bg-blue-500');
// Returns: "bg-blue-500"Cela fonctionne pour toutes les utilités Tailwind :
twMerge('px-4 py-2', 'px-8'); // Returns: "py-2 px-8"
twMerge('text-sm', 'text-lg'); // Returns: "text-lg"
twMerge('hover:bg-red-500', 'hover:bg-blue-500'); // Returns: "hover:bg-blue-500"La bibliothèque comprend également le système de modificateurs de Tailwind :
// Modifiers are handled correctly
twMerge('hover:bg-red-500 focus:bg-red-500', 'hover:bg-blue-500');
// Returns: "focus:bg-red-500 hover:bg-blue-500"Classes conditionnelles
Souvent, vous devez appliquer des classes de façon conditionnelle en fonction des props ou de l'état. La bibliothèque clsx fournit une API claire pour cela :
import clsx from 'clsx';
// Basic conditionals
clsx('base', isActive && 'active');
// Returns: "base active" (if isActive is true)
// Object syntax
clsx('base', {
'active': isActive,
'disabled': isDisabled,
});
// Arrays
clsx(['base', isLarge ? 'text-lg' : 'text-sm']);
// Mixed
clsx(
'base',
['array-item'],
{ 'object-conditional': true },
isActive && 'conditional'
);Un schéma courant consiste à fusionner un ensemble de classes par défaut avec les props entrantes, ainsi qu'avec toute logique personnalisée que nous avons :
const Component = ({ className, ...props }: ComponentProps) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div
className={cn(
"rounded-lg border bg-white shadow-sm",
isOpen && "bg-blue-500",
className
)}
{...props}
/>
);
};La fonction utilitaire cn
La fonction cn, popularisée par shadcn/ui, combine clsx et tailwind-merge pour vous offrir à la fois une logique conditionnelle et une fusion intelligente :
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}La puissance vient de l'ordre — styles de base en premier, conditionnels ensuite, surcharges utilisateur en dernier. Cela garantit un comportement prévisible tout en maintenant une personnalisation complète.
Class Variance Authority (CVA)
Pour les composants complexes avec de nombreuses variantes, gérer manuellement les classes conditionnelles devient ingérable. Class Variance Authority (CVA) fournit une API déclarative pour définir les variantes de composants.
Par exemple, voici un extrait du composant Button de shadcn/ui :
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)Bonnes pratiques
1. L'ordre est important
Appliquez toujours les classes dans cet ordre :
- Styles de base (toujours appliqués)
- Styles de variante (en fonction des props)
- Styles conditionnels (en fonction de l'état)
- Surcharges utilisateur (prop className)
className={cn(
'base-styles', // 1. Base
variant && variantStyles, // 2. Variants
isActive && 'active', // 3. Conditionals
className // 4. User overrides
)}2. Documentez vos variantes
Utilisez TypeScript et JSDoc pour documenter ce que fait chaque variante :
type ButtonProps = {
/**
* The visual style of the button
* @default "primary"
*/
variant?: 'primary' | 'secondary' | 'destructive' | 'ghost';
/**
* The size of the button
* @default "md"
*/
size?: 'sm' | 'md' | 'lg';
};3. Extraire les motifs répétés
Si vous vous retrouvez à écrire la même logique conditionnelle à répétition, extrayez-la :
export const focusRing = 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500';
export const disabled = 'disabled:pointer-events-none disabled:opacity-50';
// Use in components
className={cn(focusRing, disabled, className)}Guide de migration
Si vous migrez depuis une autre approche de stylisation, voici comment adapter les schémas courants :
Depuis CSS Modules
import styles from './Button.module.css';
<button className={`${styles.button} ${styles[variant]} ${className}`} />import { cn } from '@/lib/utils';
<button className={cn(
'px-4 py-2 rounded-lg',
variant === 'primary' && 'bg-blue-500 text-white',
className
)} />Depuis styled-components
const Button = styled.button<{ $primary?: boolean }>`
padding: 8px 16px;
background: ${props => props.$primary ? 'blue' : 'gray'};
`;function Button({ primary, className, ...props }) {
return (
<button
className={cn(
'px-4 py-2',
primary ? 'bg-blue-500' : 'bg-gray-500',
className
)}
{...props}
/>
);
}Considérations de performance
Les deux bibliothèques clsx et tailwind-merge sont hautement optimisées, mais gardez ces conseils à l'esprit :
-
Définissez les variantes en dehors des composants - les variantes CVA doivent être définies en dehors du composant pour éviter d'être recréées à chaque rendu.
-
Mémorisez les calculs complexes - si vous avez une logique conditionnelle coûteuse, envisagez de la mémoïser :
const className = useMemo(
() => cn(
baseStyles,
expensiveComputation(props),
className
),
[props, className]
);- Utilisez des variables CSS pour les valeurs dynamiques - au lieu de générer des classes dynamiquement, utilisez des variables CSS :
// Good
<div
className="bg-[var(--color)]"
style={{ '--color': dynamicColor } as React.CSSProperties}
/>
// Avoid
<div className={`bg-[${dynamicColor}]`} />La combinaison de Tailwind CSS, d'une fusion intelligente des classes et d'API de variantes fournit une base solide pour la stylisation des composants. Cette approche s'étend des simples boutons aux systèmes de design complexes tout en maintenant la prévisibilité et l'expérience développeur.