Varianti dei componenti con CVA
Utilizzi la libreria class-variance-authority per definire API di varianti tipizzate per i componenti React, mantenendo la logica delle classi pulita e prevedibile.
Varianti dei componenti con CVA è una lezione Tailwind CSS Academy gratuita su CoddyKit. Questa è la lezione 3 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Tailwind CSS Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Tailwind CSS Academy include 4 lezioni in totale.
Parti di questa lezione non sono ancora state tradotte e vengono mostrate in inglese.
The Problem With Manual Variant Logic
Building component variant systems manually — with nested if statements and class maps — scales poorly. A button with 3 colors, 3 sizes, and a disabled state requires 18+ unique class combinations to handle correctly. The logic becomes sprawling and error-prone. Class Variance Authority (CVA) provides a structured, type-safe API for defining variant class maps, reducing variant logic to a clean declarative configuration.
Installing and Importing CVA
Install class-variance-authority with npm and import the cva function into your component file. CVA works alongside your existing cn() helper — CVA handles the variant logic, and tailwind-merge handles conflict resolution when caller classes are merged in. This combination is the foundation of libraries like shadcn/ui.
# Install
npm install class-variance-authority
# Also install companions if not present
npm install clsx tailwind-merge
// lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// In your component file
import { cva, type VariantProps } from 'class-variance-authority';Defining a Component With cva
Call cva() with two arguments: a string of base classes that always apply, and an options object with a variants map. Each key in variants is a prop name, and its value is an object mapping prop values to class strings. CVA merges the base classes with the matched variant classes when the returned function is called with prop values.
import { cva } from 'class-variance-authority';
const buttonVariants = cva(
// Base classes — always applied
'inline-flex items-center justify-center rounded-lg font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:opacity-50 disabled:pointer-events-none',
{
variants: {
variant: {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
outline: 'border border-gray-300 bg-transparent hover:bg-gray-50',
ghost: 'hover:bg-gray-100 text-gray-700'
},
size: {
sm: 'h-8 px-3 text-xs',
md: 'h-10 px-4 text-sm',
lg: 'h-12 px-6 text-base'
}
},
defaultVariants: {
variant: 'primary',
size: 'md'
}
}
);Using VariantProps for TypeScript
CVA exports the VariantProps utility type that automatically infers the correct TypeScript types for your variant props from the cva definition. This means TypeScript will error if you pass an invalid variant value — like variant='purple' when only primary, secondary, outline, and ghost are defined. You get full IntelliSense autocomplete for variant values in your editor.
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva('...', { variants: { /* ... */ } });
// VariantProps extracts the type automatically
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
function Button({ variant, size, className, ...props }: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
);
}Compound Variants
CVA supports compound variants — classes that apply only when a specific combination of variant values is active. For example, you might want a special style that applies only when both variant='primary' and size='lg' are used together. This is more expressive than writing nested conditionals and keeps all variant logic in one place.
const buttonVariants = cva('base-classes', {
variants: {
variant: {
primary: 'bg-blue-600 text-white',
destructive: 'bg-red-600 text-white'
},
size: {
sm: 'h-8 px-3 text-xs',
lg: 'h-12 px-6 text-base'
}
},
compoundVariants: [
{
// Apply only when BOTH conditions are true
variant: 'primary',
size: 'lg',
class: 'shadow-lg hover:shadow-xl'
},
{
variant: 'destructive',
size: 'lg',
class: 'ring-2 ring-red-300'
}
]
});Default Variants
The defaultVariants field in the cva options specifies which variant values are used when the corresponding prop is not provided. This makes most props optional while still producing correct output. A Button component with defaultVariants: { variant: 'primary', size: 'md' } renders a medium-sized primary button when used as <Button> with no props.
const badgeVariants = cva(
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
{
variants: {
variant: {
default: 'bg-gray-100 text-gray-800',
success: 'bg-green-100 text-green-800',
warning: 'bg-yellow-100 text-yellow-800',
danger: 'bg-red-100 text-red-800'
}
},
defaultVariants: {
variant: 'default' // <Badge /> without variant prop → default style
}
}
);
// All three are valid:
<Badge /> // uses default variant
<Badge variant='success' /> // green
<Badge variant='danger' /> // redCVA for Non-Button Components
CVA is not limited to buttons — use it for any component with multiple style variants. Cards can have elevated, outlined, and ghost variants. Inputs can have default and error states. Text elements can have h1 through h6 size variants. CVA's declarative API works equally well for any component where CSS classes vary based on props.
const cardVariants = cva(
'rounded-xl overflow-hidden',
{
variants: {
variant: {
elevated: 'bg-white shadow-md hover:shadow-lg transition-shadow',
outlined: 'bg-white border border-gray-200',
filled: 'bg-gray-50'
},
padding: {
none: '',
sm: 'p-4',
md: 'p-6',
lg: 'p-8'
}
},
defaultVariants: {
variant: 'elevated',
padding: 'md'
}
}
);Composing CVA Variants
Multiple CVA definitions can be composed together. A base button style can be defined with cva, and a more specific IconButton can call the base cva function and merge its result with icon-specific classes. This composition model mirrors component composition in React and keeps each variant definition focused on a single concern.
// Base shared button classes
const baseButton = cva(
'inline-flex items-center justify-center font-medium transition-colors disabled:opacity-50',
{
variants: {
variant: {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
ghost: 'hover:bg-gray-100'
}
}
}
);
// Icon button extends base
function IconButton({ icon, variant, 'aria-label': label }) {
return (
<button
className={cn(
baseButton({ variant }),
'h-10 w-10 rounded-full p-0' // icon-specific additions
)}
aria-label={label}
>
{icon}
</button>
);
}Exporting Variants for Reuse
In component libraries, it is common to export the cva function result (the variants function) alongside the component itself. This allows other components to apply the same variant classes without importing the full component. For example, a Link component might want to apply buttonVariants({ variant: 'primary' }) to render as a button visually while remaining a semantic anchor element.
// components/Button.tsx
export const buttonVariants = cva('...base...', {
variants: {
variant: {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-100 text-gray-900'
},
size: { sm: 'h-8 px-3', md: 'h-10 px-4', lg: 'h-12 px-6' }
}
});
export function Button({ variant, size, className, ...props }) {
return <button className={cn(buttonVariants({ variant, size }), className)} {...props} />;
}
// In another file: Link styled as a button
import { buttonVariants } from './Button';
<a href='/signup' className={cn(buttonVariants({ variant: 'primary', size: 'md' }))}>
Sign Up
</a>CVA With Responsive Variants
Tailwind's responsive prefixes can be included inside CVA class strings just like any other utility. This means you can define variants that include responsive behavior. For example, a layout variant might apply flex-col md:flex-row — the CVA system does not need to know about Tailwind's breakpoints; they are just class strings.
const stackVariants = cva(
'flex gap-4',
{
variants: {
direction: {
vertical: 'flex-col',
horizontal: 'flex-row',
responsive: 'flex-col sm:flex-row' // breakpoint in variant
},
align: {
start: 'items-start',
center: 'items-center',
end: 'items-end'
}
},
defaultVariants: {
direction: 'vertical',
align: 'start'
}
}
);
// Usage
<div className={stackVariants({ direction: 'responsive', align: 'center' })}>
{/* Children stack vertically on mobile, horizontally on sm+ */}
</div>Real-World CVA Component Library
A mature CVA setup defines all primitive components — Button, Badge, Input, Card, Alert — each with their own cva definitions. These components are collected into a components/ui folder (as seen in shadcn/ui), each file exporting both the React component and its variants function. This structure makes the variant system discoverable, testable, and easy to extend.
// components/ui/alert.tsx
import { cva, type VariantProps } from 'class-variance-authority';
export const alertVariants = cva(
'relative w-full rounded-lg border p-4 text-sm',
{
variants: {
variant: {
default: 'bg-white text-gray-900 border-gray-200',
destructive: 'bg-red-50 text-red-800 border-red-200',
success: 'bg-green-50 text-green-800 border-green-200'
}
},
defaultVariants: { variant: 'default' }
}
);
export function Alert({ variant, className, children, ...props }) {
return (
<div className={cn(alertVariants({ variant }), className)} role='alert' {...props}>
{children}
</div>
);
}Quick Check
Test your understanding of Tailwind CSS Mastery concepts from this lesson.
Lesson Recap
In this lesson you learned: cva() defines variant maps with base classes plus conditional class groups, VariantProps automatically derives TypeScript types from the cva definition, and compoundVariants apply classes only when specific combinations of variants are active. Next up we explore how tailwind-merge prevents class conflicts when composing components.
Domande Frequenti
La lezione «Varianti dei componenti con CVA» è gratuita?
Sì — il testo completo di «Varianti dei componenti con CVA» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Tailwind CSS Academy, passa a CoddyKit PRO. Il corso Tailwind CSS Academy include 4 lezioni in totale.
Cosa imparerò in «Varianti dei componenti con CVA»?
Utilizzi la libreria class-variance-authority per definire API di varianti tipizzate per i componenti React, mantenendo la logica delle classi pulita e prevedibile. Eserciti Tailwind CSS Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare Tailwind CSS Academy?
Non è richiesta alcuna esperienza precedente. Tailwind CSS Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 3 di 4.
Quanto tempo richiede la lezione «Varianti dei componenti con CVA»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione Tailwind CSS Academy?
Sì. Ogni lezione Tailwind CSS Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Configurare Tailwind in Next.js
- Classi condizionali in React
- Varianti dei componenti con CVA
- Evitare conflitti tra classi con tailwind-merge