Merge classes with cn()

cn() drops falsy classes and lets the last conflicting utility win.

From shadcn (Vercel plugin): every component merges classes with cn().

Override via className

Type classes or pick a preset, then reverse the CSS order.

String concatenationBad
class

className={`${base} ${className}`}
cn()Good
twMerge

className={cn(base, className)}
// lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

// components/ui/button.tsx
export function Button({ className, ...props }: ButtonProps) {
  return (
    <button
      className={cn("rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground", className)}
      {...props}
    />
  )
}

clsx drops, twMerge dedupes

Toggle the flags, or click either Pin button.

Template stringBad
class

`${base} ${isActive && active} ${isDisabled && disabled} ${className}`
cn()Good
clsx
twMerge

cn(base, isActive && active, isDisabled && disabled, className)
<button
  aria-pressed={isActive}
  disabled={isDisabled}
  className={cn(
    "rounded-md px-4 py-2 text-sm bg-secondary text-secondary-foreground",
    isActive && "bg-primary text-primary-foreground",
    isDisabled && "opacity-50 cursor-not-allowed",
    className
  )}
/>

Why it matters, pros and cons

Why it matters

Tailwind resolves px-4 px-2 by stylesheet order, not class order.

Pros

  • A caller’s className always beats defaults.
  • No false or undefined in the DOM.
  • The class list is exactly what applies.

Cons

  • twMerge parses strings at runtime; long lists cost.
  • Custom scales need extendTailwindMerge or merge wrongly.
  • A static string with no overrides needs no cn().