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
cn(...inputs: ClassValue[]): string;| prop | type | default | |
|---|---|---|---|
…inputs | ClassValue[] | — | 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.
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.
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.
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.
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-col | conflicts with col-start, col-end and col-span: it sets all of them at once |
margo-col-start | the start line |
margo-col-end | the end line; conflicts with col-span, which expresses the same edge |
margo-col-span | the length; conflicts with col-end |
margo-row | conflicts with row-start and row-span |
margo-row-start | the start row |
margo-row-span | the number of rows |
margo-gutter | conflicts with gutter-x and gutter-y |
margo-gutter-x | column gap |
margo-gutter-y | row gap |
margo-padding | side padding of a grid |
font-size | the 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.
// 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");