cn

Joins class names conditionally and resolves the conflicts between them, so an override always wins on merit rather than on emission order.

API

API

tsx
cn(...inputs: ClassValue[]): string;
proptypedefault
…inputsClassValue[]strings, arrays, and objects keyed by class name

Examples

Usage

It is two libraries in one call: clsx flattens the arguments, dropping anything falsy, and tailwind-merge then removes the classes that a later class already overrides.

tsx
cn("px-3 py-2", isActive && "bg-main", { "opacity-50": disabled }, className);

Details

Why merging and not concatenating

Two Tailwind classes from the same family produce two rules with identical specificity, so the winner is whichever the stylesheet happens to emit last — an order you do not control and that can change between builds. Concatenating px-3 and px-5 leaves that decision to chance; merging resolves it by argument order, which is the order you wrote.

tsx
cn("px-3", "px-5");
// "px-5"

cn("text-sm text-medium", "text-on-main");
// "text-sm text-on-main"

This is what makes every component of the kit overridable in the same way: the base classes come first, your className comes last, and last wins.

tsx
export const Card = <T extends ElementType = "div">({ className, children, ...rest }: CardProps<T>) => (
  <Tag {...Tag.forward<T>(rest)} className={cn(cardBaseClassName, cardFocusClassName, className)}>
    {children}
  </Tag>
);

The grid families

tailwind-merge only knows the families it ships with, so the margo grid utilities would otherwise pile up instead of replacing each other. cn teaches it those families, including which ones contradict each other: margo-col-4 sets start and end at once, so it clears an explicit start, end or span written before it.

tsx
cn("margo-col-span-4", "margo-col-span-6");
// "margo-col-span-6"

cn("margo-col-4", "margo-col-start-2 margo-col-end-8");
// "margo-col-start-2 margo-col-end-8"
family
margo-colconflicts with col-start, col-end and col-span: it sets all of them at once
margo-col-startthe start line
margo-col-endthe end line; conflicts with col-span, which expresses the same edge
margo-col-spanthe length; conflicts with col-end
margo-rowconflicts with row-start and row-span
margo-row-startthe start row
margo-row-spanthe number of rows
margo-gutterconflicts with gutter-x and gutter-y
margo-gutter-xcolumn gap
margo-gutter-yrow gap
margo-paddingside padding of a grid
font-sizethe built-in family, extended with text-mc

The values recognised in those families are a column index, or the two named lines full and content — which is why margo-col-start-content correctly replaces a previous margo-col-start-full instead of coexisting with it.

Merging is not generating

cn runs at runtime; Tailwind generates CSS by scanning your source as text. A class assembled from a template literal exists at runtime but was never seen by the scanner, so the rule behind it does not exist. Branch between whole class names instead.

tsx
// never generated: Tailwind sees a fragment, not a class
cn(`text-${tone}`);

// generated: both classes exist in the source
cn(tone === "muted" ? "text-medium" : "text-on-main");