Class Variance Authority (CVA)
When to Use
Use CVA when a component has 2+ orthogonal variant dimensions, needs compound variants, or requires TypeScript autocompletion on variant props.
Decision
| Situation |
Use CVA? |
Why |
| Component with 2+ variant dimensions (size + intent) |
Yes |
Structured; prevents combinatorial class explosion |
| Compound variants (specific combo = extra styles) |
Yes |
CVA handles this natively |
| TypeScript autocompletion on variant props |
Yes |
VariantProps type helper |
| Single boolean variant |
Maybe |
cx from CVA may be enough |
| Data-driven values (dynamic width, runtime colors) |
No |
Use inline style={} for truly dynamic values |
Pattern
import { cva, cx, type VariantProps } from 'class-variance-authority';
const button = cva(
// Base classes — always applied
'inline-flex items-center font-semibold rounded-lg transition-colors focus-visible:outline-2',
{
variants: {
intent: {
primary: 'bg-brand-500 text-white hover:bg-brand-600',
ghost: 'bg-transparent text-brand-500 hover:bg-brand-50',
danger: 'bg-red-600 text-white hover:bg-red-700',
},
size: {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
},
},
compoundVariants: [
{ intent: 'danger', size: 'sm', class: 'ring-1 ring-red-400' },
],
defaultVariants: {
intent: 'primary',
size: 'md',
},
}
);
type ButtonProps = VariantProps<typeof button>;
// Both 'class' and 'className' props are valid
<button class={button({ intent: 'ghost', size: 'sm' })}>Cancel</button>
<button className={button({ intent: 'primary' })}>Submit</button>
CVA Exports (v0.7.1)
| Export |
Purpose |
cva |
Creates a variant-aware class generator function |
cx |
Re-export of clsx; use for ad-hoc class merging without variant logic |
VariantProps |
TypeScript type helper for inferring prop types from a CVA definition |
cx for Ad-hoc Composition
import { cx } from 'class-variance-authority';
const cls = cx('base-class', isActive && 'bg-brand-500', { 'font-bold': isPrimary });
Common Mistakes
- Wrong: Dynamic class string construction inside CVA —
bg-${color}-500 is invisible to Tailwind's scanner. Right: Always use complete class strings in lookup objects.
- Wrong: Creating CVA variants for styles that aren't real design variants. Right:
style={{ width: dynamicWidth }} is correct for data-driven values.
- Wrong: Using
@apply to build a class system when you have React/Vue/Twig components available. Right: The component IS the abstraction; use CVA inside the component.
See Also