Lib
CN (Class Names) Utility
Combine class names and intelligently merge Tailwind classes using clsx and tailwind-merge.
CN (Class Names) Utility
cn is a tiny helper that composes class names and then intelligently merges Tailwind classes to avoid conflicts. It wraps clsx for expressive class composition and tailwind-merge for conflict resolution (e.g., p-2 vs p-4, text-red-500 vs text-blue-500).
Use cn for any place you’d write a className string that can be composed from multiple parts (props, conditionals, and local styles).
Why use cn?
- Compose static, conditional, and array-based classes with a single call
- Avoid Tailwind conflicts automatically (the “rightmost” value wins after merge)
- Keep components readable by centralizing class logic
Usage
Basic usage (compose and merge):
import { cn } from '@loke/design-system/cn';
function MyComponent({ isActive, className }: { isActive?: boolean; className?: string }) {
return (
<div
className={cn(
'rounded border p-2 text-sm',
isActive && 'bg-primary text-primary-foreground',
className,
)}
>
Hello
</div>
);
}Merging conflicting Tailwind classes (last one wins):
import { cn } from '@loke/design-system/cn';
const classes = cn('p-2 text-red-500', 'p-4 text-blue-500');
// => "p-4 text-blue-500"Conditional objects and arrays:
import { cn } from '@loke/design-system/cn';
const size = 'lg' as 'sm' | 'md' | 'lg';
const classes = cn(
'inline-flex items-center',
['rounded', 'font-medium'],
{
'px-2 py-1 text-xs': size === 'sm',
'px-3 py-1.5 text-sm': size === 'md',
'px-4 py-2 text-base': size === 'lg',
},
);Combining props.className with local styles:
import { cn } from '@loke/design-system/cn';
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: 'default' | 'outline' | 'ghost';
};
export function Button({ className, variant = 'default', ...props }: ButtonProps) {
return (
<button
{...props}
className={cn(
'inline-flex items-center justify-center rounded-md transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
{
'bg-primary text-primary-foreground hover:bg-primary/90': variant === 'default',
'border border-input bg-background hover:bg-muted': variant === 'outline',
'hover:bg-muted': variant === 'ghost',
},
className,
)}
/>
);
}API
Prop
Type
Function signature:
// wraps clsx + tailwind-merge
function cn(...inputs: ClassValue[]): string;How it works
clsxflattens and composes the inputs:
- Strings of classes
- Arrays of classes
- Objects mapping class -> boolean
tailwind-mergetakes the composed result and removes/merges conflicting Tailwind classes so you don’t end up with redundant or contradictory utilities.
Tips
- Keep
classNameprops last incn(...)to allow consumers to override defaults - Prefer semantic variants in your component logic, then map them to Tailwind utilities via
cn - Use arrays/objects when conditions are complex; it keeps the call site readable
Examples
Prevent Tailwind conflicts when combining variants:
const pill = cn(
'rounded-full text-xs',
'px-2 py-1',
'bg-secondary text-secondary-foreground',
// later variant overrides color safely
'bg-destructive text-destructive-foreground',
);
// => "rounded-full text-xs px-2 py-1 bg-destructive text-destructive-foreground"Merging dynamic spacing:
function Card({ compact }: { compact?: boolean }) {
return (
<div
className={cn(
'rounded-md border bg-card text-card-foreground',
compact ? 'p-2' : 'p-4',
)}
>
Content
</div>
);
}Notes
cnis lightweight and safe to use in hot paths; still, if you’re recomputing large class sets repeatedly, memoize where appropriate.- The resolution strategy is Tailwind-specific via
tailwind-merge. For non-Tailwind projects,clsxalone is fine, but you’ll lose conflict resolution.