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
CSS order
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
classNamealways beats defaults. - No
falseorundefinedin the DOM. - The class list is exactly what applies.
Cons
- twMerge parses strings at runtime; long lists cost.
- Custom scales need
extendTailwindMergeor merge wrongly. - A static string with no overrides needs no
cn().