Variant Management with CVA
When to Use
Use when a component has multiple dimensions of variation (size, color/intent, state) that combine in predictable ways. CVA makes these combinations type-safe and conflict-free.
Decision
| If you need... | Use... | Why |
|---|---|---|
| Independent size + color variants | CVA variants object |
Each dimension declared separately; TypeScript infers all combos |
| Special styles for one variant combo | CVA compoundVariants |
Targets exact combinations without conditional logic in JSX |
| Variant styles across component slots | tailwind-variants tv() with slots |
CVA is single-element; TV handles multiple DOM nodes per variant |
| Extend a base component's variants | Extract cva() call → compose in child |
Override specific variants; keep base's defaults |
Pattern
Full CVA with compound variants:
const alertVariants = cva(
'relative w-full rounded-lg border p-4',
{
variants: {
variant: {
default: 'bg-background text-foreground',
destructive: 'border-destructive/50 text-destructive dark:border-destructive',
},
},
compoundVariants: [
// compoundVariants apply when multiple variant conditions are ALL true
{ variant: 'destructive', className: '[&>svg]:text-destructive' },
],
defaultVariants: { variant: 'default' },
}
);
tailwind-variants with slots (for compound components):
import { tv } from 'tailwind-variants';
const card = tv({
slots: {
root: 'rounded-lg border bg-card shadow-sm',
header: 'flex flex-col space-y-1.5 p-6',
title: 'text-2xl font-semibold leading-none',
body: 'p-6 pt-0',
},
variants: {
size: {
sm: { root: 'p-4', title: 'text-lg' },
lg: { root: 'p-8', title: 'text-3xl' },
},
},
});
// Usage: const { root, header, title, body } = card({ size: 'sm' });
Common Mistakes
- Wrong: Nesting variant logic in JSX with ternaries → Right: Put all variant logic in
cva(); ternaries in JSX are hard to read and untyped - Wrong: Forgetting
VariantProps<typeof variantFn>on the interface → Right: Always extract types from CVA; without it variant props become untypedstring - Wrong: Overusing
compoundVariants→ Right: If every variant combo needs one, the variant design is wrong; reconsider the structure - Wrong: Using tailwind-variants for simple single-element components → Right: CVA is simpler when there are no slots
- Wrong: Defining
cva()calls inside the component function → Right: Extract to module scope so they're created once, not on every render
See Also
- Tailwind Integration
- Design Token Consumption
- Reference: CVA — Variants
- Reference: tailwind-variants — Slots