cn

Merge and deduplicate Tailwind CSS class names with automatic conflict resolution.

cn

cn is a utility function that merges multiple class name strings or arrays into a single deduplicated string. It intelligently handles Tailwind CSS conflicts, ensuring that utility classes don't override each other unexpectedly.

cn combines clsx for conditional class support with tailwind-merge for smart Tailwind conflict resolution.


API

function cn(...inputs: (string | undefined | null | Record<string, boolean>)[]): string

Returns a deduplicated class string with Tailwind conflicts resolved.


Features

  • Conditional classes: Pass objects with boolean values to conditionally include classes
  • Deduplication: Removes duplicate class names
  • Tailwind conflict resolution: Automatically resolves conflicting Tailwind utilities (e.g., px-2 + px-4px-4)
  • Null/undefined handling: Safely ignores null, undefined, and falsy values
  • Array spreading: Flattens nested arrays of classes

Usage

Basic merging

import { cn } from '@loke/design-system/cn';

// String concatenation with deduplication
const classes = cn('px-2 py-1', 'px-4');
// Result: "py-1 px-4"  (px-4 overrides px-2)

Conditional classes

import { cn } from '@loke/design-system/cn';

function Button({ variant = 'default', disabled = false }) {
  return (
    <button
      className={cn(
        'px-4 py-2 rounded font-medium',
        variant === 'outline' && 'border border-blue-500',
        disabled && 'opacity-50 pointer-events-none'
      )}
    >
      Click me
    </button>
  );
}

Object notation

const classes = cn('base-class', {
  'active-class': isActive,
  'disabled-class': isDisabled,
});

With component variants (CVA)

import { cn } from '@loke/design-system/cn';
import { cva, type VariantProps } from 'class-variance-authority';

const buttonVariants = cva('px-4 py-2', {
  variants: {
    size: {
      sm: 'text-sm',
      md: 'text-base',
    },
  },
});

function Button({ size, className }: VariantProps<typeof buttonVariants> & { className?: string }) {
  return (
    <button className={cn(buttonVariants({ size }), className)}>
      Custom Button
    </button>
  );
}

Array spreading

const baseClasses = ['px-4', 'py-2'];
const classes = cn(baseClasses, 'rounded', { 'border border-blue': true });
// Handles nested arrays gracefully

Why use cn instead of template strings?

// ❌ Without cn: px-2 and px-4 both present (conflicting)
const classes = 'px-2 py-1 ' + (variant === 'outline' ? 'px-4' : '');

// ✅ With cn: px-4 intelligently overrides px-2
const classes = cn('px-2 py-1', variant === 'outline' && 'px-4');

Common patterns

Component styling with props

interface CardProps {
  padding?: 'sm' | 'md' | 'lg';
  hover?: boolean;
  className?: string;
}

function Card({ padding = 'md', hover = false, className }: CardProps) {
  const paddingMap = {
    sm: 'p-2',
    md: 'p-4',
    lg: 'p-6',
  };

  return (
    <div
      className={cn(
        'bg-white rounded-lg shadow',
        paddingMap[padding],
        hover && 'hover:shadow-lg transition-shadow',
        className
      )}
    >
      {/* content */}
    </div>
  );
}

Dark mode with variants

const classes = cn(
  'text-black bg-white',
  'dark:text-white dark:bg-black'
);

Responsive utilities

const classes = cn(
  'grid gap-4',
  'sm:grid-cols-2',
  'md:grid-cols-3',
  'lg:grid-cols-4'
);

Troubleshooting

Classes not applying?

  • Ensure the class names are valid Tailwind utilities
  • Check that the CSS file is imported (Tailwind must have opportunity to generate the classes)
  • Verify there's no conflicting CSS elsewhere

Unexpected overrides?

  • tailwind-merge resolves conflicts intelligently but only for known Tailwind utilities
  • Custom classes without Tailwind prefixes may not be deduplicated

Performance?

  • cn is optimized and safe to call on every render
  • The cost is minimal for typical use cases

Import

import { cn } from '@loke/design-system/cn';