Stilizzazione
Stilizzazione condizionale e componibile con classi Tailwind.
Le moderne librerie di componenti hanno bisogno di sistemi di stilizzazione flessibili che possano gestire requisiti complessi senza compromettere l'esperienza dello sviluppatore. La combinazione di Tailwind CSS con un'unione intelligente delle classi è emersa come un pattern potente per costruire componenti personalizzabili.
Questo approccio risolve la tensione fondamentale tra fornire valori predefiniti sensati e consentire una personalizzazione completa - una sfida che ha afflitto le librerie di componenti per anni.
Il problema degli stili tradizionali
Gli approcci CSS tradizionali spesso portano a guerre di specificità, conflitti di stile e override imprevedibili. Quando passi className="bg-blue-500" a un componente che ha già bg-red-500, quale prevale?
Senza un'adeguata gestione, entrambe le classi si applicano e il risultato dipende da molti fattori - l'ordine di sorgente del CSS, la specificità delle classi, l'algoritmo di unione delle classi del bundler, ecc.
Unire le classi in modo intelligente
La libreria tailwind-merge risolve questo comprendendo la struttura delle classi di Tailwind e risolvendo i conflitti in modo intelligente. Quando due classi mirano alla stessa proprietà CSS, mantiene solo l'ultima.
// 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"Questo funziona per tutte le utility di 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 libreria comprende anche il sistema di modificatori di 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"Classi condizionali
Spesso è necessario applicare classi condizionalmente in base a props o state. La libreria clsx fornisce un'API pulita per questo:
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 pattern comune è unire un set predefinito di classi con le props in arrivo, oltre a qualsiasi logica personalizzata che abbiamo:
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 funzione di utilità cn
La funzione cn, resa popolare da shadcn/ui, combina clsx e tailwind-merge per offrirti sia la logica condizionale sia l'unione intelligente:
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}La potenza deriva dall'ordine - prima gli stili base, poi i condizionali, infine le sovrascritture dell'utente. Questo garantisce un comportamento prevedibile mantenendo la piena personalizzazione.
Class Variance Authority (CVA)
Per componenti complessi con molte varianti, gestire manualmente le classi condizionali diventa ingombrante. Class Variance Authority (CVA) fornisce un'API dichiarativa per definire le varianti dei componenti.
Ad esempio, ecco un estratto dal componente Button di 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",
},
}
)Migliori pratiche
1. L'ordine è importante
Applica sempre le classi in questo ordine:
- Stili di base (sempre applicati)
- Stili delle varianti (in base alle props)
- Stili condizionali (in base allo state)
- Sovrascritture dell'utente (prop className)
className={cn(
'base-styles', // 1. Base
variant && variantStyles, // 2. Variants
isActive && 'active', // 3. Conditionals
className // 4. User overrides
)}2. Documenta le tue varianti
Usa TypeScript e JSDoc per documentare cosa fa ogni 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. Estrai pattern ripetuti
Se ti ritrovi a scrivere ripetutamente la stessa logica condizionale, estraila:
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)}Guida alla migrazione
Se stai migrando da un diverso approccio di stilizzazione, ecco come adattare i pattern comuni:
Da 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
)} />Da 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}
/>
);
}Considerazioni sulle prestazioni
Sia clsx che tailwind-merge sono altamente ottimizzati, ma tieni a mente questi consigli:
-
Definire le varianti al di fuori dei componenti - Le varianti CVA dovrebbero essere definite al di fuori del componente per evitare la ricreazione ad ogni render.
-
Memoizza i calcoli complessi - Se hai logica condizionale costosa, considera di memoizzarla:
const className = useMemo(
() => cn(
baseStyles,
expensiveComputation(props),
className
),
[props, className]
);- Usa variabili CSS per valori dinamici - Invece di generare classi dinamicamente, usa variabili CSS:
// Good
<div
className="bg-[var(--color)]"
style={{ '--color': dynamicColor } as React.CSSProperties}
/>
// Avoid
<div className={`bg-[${dynamicColor}]`} />La combinazione di Tailwind CSS, un'unione intelligente delle classi e le API per le varianti fornisce una base solida per la stilizzazione dei componenti. Questo approccio scala dai semplici pulsanti a sistemi di design complessi mantenendo prevedibilità ed esperienza dello sviluppatore.