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.

Without 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"
With 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"

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:

Using 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 pattern comune è unire un set predefinito di classi con le props in arrivo, oltre a qualsiasi logica personalizzata che abbiamo:

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 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:

lib/utils.ts
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:

@/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",
    },
  }
)

Migliori pratiche

1. L'ordine è importante

Applica sempre le classi in questo ordine:

  1. Stili di base (sempre applicati)
  2. Stili delle varianti (in base alle props)
  3. Stili condizionali (in base allo state)
  4. 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:

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)}

Guida alla migrazione

Se stai migrando da un diverso approccio di stilizzazione, ecco come adattare i pattern comuni:

Da CSS Modules

Before - CSS Modules
import styles from './Button.module.css';

<button className={`${styles.button} ${styles[variant]} ${className}`} />
After - cn + Tailwind
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

Before - styled-components
const Button = styled.button<{ $primary?: boolean }>`
  padding: 8px 16px;
  background: ${props => props.$primary ? 'blue' : 'gray'};
`;
After - cn + Tailwind
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:

  1. 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.

  2. Memoizza i calcoli complessi - Se hai logica condizionale costosa, considera di memoizzarla:

const className = useMemo(
  () => cn(
    baseStyles,
    expensiveComputation(props),
    className
  ),
  [props, className]
);
  1. Usa variabili CSS per valori dinamici - Invece di generare classi dinamicamente, usa variabili CSS:
Prefer CSS variables
// 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.