mirror of
https://github.com/vercel/components.build.git
synced 2026-09-14 20:06:39 +08:00
55fae375a4
* Update Geistdocs * Update geistdocs.tsx * Run translation script * Delete toc.tsx * Update route.ts * Update route.ts
280 lines
9.4 KiB
Plaintext
280 lines
9.4 KiB
Plaintext
---
|
|
title: Stylisation
|
|
description: 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.
|
|
|
|
```tsx title="Sans tailwind-merge"
|
|
// Both bg-red-500 and bg-blue-500 apply - unpredictable result
|
|
<Button className="bg-blue-500" />
|
|
// Renders: className="bg-red-500 bg-blue-500"
|
|
```
|
|
|
|
```tsx title="Avec tailwind-merge"
|
|
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 :
|
|
|
|
```tsx
|
|
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 :
|
|
|
|
```tsx
|
|
// 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 :
|
|
|
|
```tsx title="Utilisation de clsx"
|
|
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 :
|
|
|
|
```tsx title="component.tsx"
|
|
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](https://ui.shadcn.com/), combine `clsx` et `tailwind-merge` pour vous offrir à la fois une logique conditionnelle et une fusion intelligente :
|
|
|
|
```tsx title="lib/utils.ts"
|
|
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)](https://cva.style/docs) fournit une API déclarative pour définir les variantes de composants.
|
|
|
|
Par exemple, voici un extrait du composant [Button](https://ui.shadcn.com/docs/components/button) de shadcn/ui :
|
|
|
|
```tsx title="@/components/ui/button.tsx"
|
|
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 :
|
|
1. Styles de base (toujours appliqués)
|
|
2. Styles de variante (en fonction des props)
|
|
3. Styles conditionnels (en fonction de l'état)
|
|
4. Surcharges utilisateur (prop className)
|
|
|
|
```tsx
|
|
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 :
|
|
|
|
```tsx
|
|
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 :
|
|
|
|
```tsx title="utils/styles.ts"
|
|
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
|
|
|
|
```tsx title="Avant - CSS Modules"
|
|
import styles from './Button.module.css';
|
|
|
|
<button className={`${styles.button} ${styles[variant]} ${className}`} />
|
|
```
|
|
|
|
```tsx title="Après - cn + Tailwind"
|
|
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
|
|
|
|
```tsx title="Avant - styled-components"
|
|
const Button = styled.button<{ $primary?: boolean }>`
|
|
padding: 8px 16px;
|
|
background: ${props => props.$primary ? 'blue' : 'gray'};
|
|
`;
|
|
```
|
|
|
|
```tsx title="Après - cn + Tailwind"
|
|
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 :
|
|
|
|
1. **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.
|
|
|
|
2. **Mémorisez les calculs complexes** - si vous avez une logique conditionnelle coûteuse, envisagez de la mémoïser :
|
|
|
|
```tsx
|
|
const className = useMemo(
|
|
() => cn(
|
|
baseStyles,
|
|
expensiveComputation(props),
|
|
className
|
|
),
|
|
[props, className]
|
|
);
|
|
```
|
|
|
|
3. **Utilisez des variables CSS pour les valeurs dynamiques** - au lieu de générer des classes dynamiquement, utilisez des variables CSS :
|
|
|
|
```tsx title="Privilégiez les 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. |