Typed Variants and Conflict-Free Class Merging
clsx, cva & tailwind-merge
The standard toolkit for component variants and overridable class props: clsx, cva, and tailwind-merge.
What you'll learn
- Use clsx or cn for conditional classes
- Define typed variants with cva
- Resolve conflicting utilities with tailwind-merge
Three tiny libraries form the de-facto standard for building robust Tailwind component APIs: clsx for conditionals, tailwind-merge for conflict resolution, and cva for typed variants.
clsx: Conditional Classes
clsx joins truthy class fragments and skips falsy ones, keeping JSX readable:
import { clsx } from 'clsx';
clsx('p-4', isActive && 'bg-blue-600', { 'opacity-50': disabled }); tailwind-merge: Conflict-Free Overrides
When two conflicting utilities are present, the last one in the CSS wins — not the last one in your string. tailwind-merge fixes this so a className prop can reliably override:
import { twMerge } from 'tailwind-merge';
twMerge('p-2', 'p-4'); // -> 'p-4'
twMerge('bg-red-500', 'bg-blue-500'); // -> 'bg-blue-500' The conventional helper combines both into one cn function:
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export const cn = (...args) => twMerge(clsx(args)); cva: Typed Variants
cva (class-variance-authority) defines a base plus named variant maps, giving you a typed component API:
import { cva } from 'class-variance-authority';
const button = cva('rounded-md font-medium', {
variants: {
intent: { primary: 'bg-blue-600 text-white', ghost: 'bg-transparent' },
size: { sm: 'px-3 py-1 text-sm', md: 'px-4 py-2 text-base' },
},
defaultVariants: { intent: 'primary', size: 'md' },
});
<button className={cn(button({ intent: 'ghost' }), className)} />