clsx, cva & tailwind-merge

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.

4 min read Level 3/5 #tailwind#clsx#cva
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)} />
Building a Design System →