Nei progetti di grandi dimensioni, la gestione dello stile tramite classi utility di Tailwind CSS all’interno dell’attributo class genera concatenazioni di stringhe complesse e difficili da manutenere. Con l’aumentare delle combinazioni di stato di un componente (varianti visive, dimensioni, stati di caricamento o disabilitazione), la logica di calcolo delle classi all’interno del template aumenta di complessità. Inoltre, la mancanza di tipizzazione nativa per le opzioni di stile impedisce al compilatore di rilevare valori non validi durante la fase di sviluppo.
In questo articolo vedremo come costruire componenti Vue 3 totalmente Type-Safe, dove il compilatore TypeScript ci impedisce di passare props errate, combinando Tailwind CSS, CVA (Class Variance Authority) e tailwind-merge.
1. Librerie di supporto
La gestione deterministica delle classi richiede tre strumenti principali:
cva(Class Variance Authority): permette di dichiarare le varianti del componente tramite un oggetto JavaScript, generando classi in modo deterministico.clsx: gestisce la costruzione di stringhe di classi in base a valori booleani o espressioni.tailwind-merge: Risolve i conflitti tra classi di Tailwind in modo intelligente, garantendo la sovrascrittura corretta delle regole (es. se passipx-4a un componente che internamente hapx-2,tailwind-mergecapisce chepx-4deve vincere senza affidarsi all’ordine nel CSS).
Installazione delle dipendenze:
bash
npm install class-variance-authority clsx tailwind-merge
2. Il pattern cn (Class Name Utility)
Creiamo un’utility centralizzata per combinare la logica condizionale di clsx con il meccanismo di deduplicazione e risoluzione dei conflitti di tailwind-merge.
File src/utils/cn.ts:
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
/**
* Unisce le classi CSS in modo condizionale e risolve i conflitti di Tailwind CSS.
* @param inputs Array di classi CSS, oggetti condizionali o undefined
* @returns Stringa di classi CSS unita e deduplicata
*/
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}
3. Definire le varianti con CVA
La definizione delle classi Tailwind viene isolata dal componente Vue e collocata in un file di configurazione dedicato, che prende il posto degli operatori ternari all’interno dei template. Questo approccio consente anche di estrarre automaticamente i tipi per TypeScript.
File src/components/ui/button/buttonVariants.ts:
import { cva, type VariantProps } from "class-variance-authority";
export const buttonVariants = cva(
// 1. Classi base: sempre applicate
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:pointer-events-none disabled:opacity-50",
{
// 2. Varianti del componente
variants: {
variant: {
primary: "bg-brand-600 text-white hover:bg-brand-700 shadow-sm",
secondary: "bg-slate-100 text-slate-900 hover:bg-slate-200",
outline:
"border border-slate-300 bg-transparent hover:bg-slate-100 text-slate-700",
danger: "bg-red-600 text-white hover:bg-red-700",
ghost: "hover:bg-slate-100 hover:text-slate-900 text-slate-600"
},
size: {
sm: "h-8 px-3 text-xs",
md: "h-10 px-4 py-2",
lg: "h-12 px-8 text-base"
}
},
// 3. Fallback di default se la prop non viene passata
defaultVariants: {
variant: "primary",
size: "md"
}
}
);
// Estraiamo le interfacce TypeScript direttamente dalla dichiarazione CVA!
export type ButtonVariants = VariantProps;
L’esportazione del tipo ButtonVariants estrae un tipo unione derivato direttamente dalle chiavi definite nell’oggetto variants. In questo modo TypeScript sa che variant può essere solo “primary" | "secondary" | "outline" | "danger" | "ghost". Se si cerca di passare al componente variant="success", la build fallirà.
4. Il Componente Vue: <BaseButton>
Ora integriamo tutto in un componente Vue che importa i tipi e la funzione di calcolo delle classi per definire l’interfaccia delle props. Grazie alle feature introdotte in Vue 3.3+ (che permette di usare tipi importati esternamente dentro defineProps), l’implementazione è molto pulita.
File src/components/ui/button/BaseButton.vue:
5. Developer Experience
L’integrazione con TypeScript garantisce il controllo dei tipi in fase di compilazione e il supporto dell’autocompletamento nell’ambiente di sviluppo.
Salva modifiche
Elimina
Bottone Custom
Conclusioni
L’esternalizzazione delle regole di stile mediante Class Variance Authority consente di separare la definizione della struttura HTML del componente dalla sua configurazione estetica. La definizione di API interne rende lo sviluppo più veloce, prevedibile e a prova d’errore. Questo modello garantisce tre proprietà principali:
- Tipizzazione rigorosa: rilevamento degli errori durante l’assegnazione delle props relative allo stile.
- Modularità: separazione netta tra la sintassi del template Vue e la logica delle varianti CSS.
- Risoluzione deterministica delle classi: prevenzione dei conflitti CSS nell’override delle props grazie all’integrazione di
tailwind-merge.
