Utility Functions

SmoothUI utility functions for React and Tailwind CSS. The cn() utility for merging class names with clsx and tailwind-merge for conflict-free styling.

Last updated: July 9, 2026

Overview

SmoothUI provides utility functions that simplify common patterns in React development. These utilities are used throughout the component library and are available for your own components.


cn() - Class Name Utility

The cn() function merges class names intelligently, combining the power of clsx for conditional classes and tailwind-merge for resolving Tailwind CSS conflicts.

Installation

The cn() utility is included with any SmoothUI component installation. You can also install it directly:

pnpm add clsx tailwind-merge

Then create the utility:

// lib/utils.ts
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

Usage

import { cn } from "@/lib/utils";

function Button({ className, variant, ...props }) {
  return (
    <button
      className={cn(
        // Base styles
        "px-4 py-2 rounded-md font-medium",
        // Variant styles
        variant === "primary" && "bg-blue-500 text-white",
        variant === "secondary" && "bg-gray-200 text-gray-900",
        // Consumer overrides (always wins)
        className
      )}
      {...props}
    />
  );
}

Why cn()?

Problem: Tailwind Class Conflicts

Without cn(), Tailwind classes can conflict unpredictably:

// Without cn() - which padding wins? Unpredictable!
<div className={`p-4 ${className}`} />
// If className="p-2", result is "p-4 p-2" - browser uses last one (p-2)
// But this isn't guaranteed and varies by property

Solution: Smart Merging

cn() intelligently resolves conflicts:

// With cn() - consumer override always wins
<div className={cn("p-4", className)} />
// If className="p-2", result is "p-2" - clean, predictable

Features

Conditional Classes

cn(
  "base-class",
  isActive && "active-class",        // Boolean condition
  !isDisabled && "enabled-class",    // Negated condition
  error ? "error-class" : "success"  // Ternary
)

Object Syntax

cn({
  "bg-blue-500": variant === "primary",
  "bg-gray-500": variant === "secondary",
  "opacity-50 cursor-not-allowed": isDisabled,
})

Array Syntax

cn([
  "base-styles",
  conditionalStyles,
  [nestedArray, "also-works"]
])

Tailwind Conflict Resolution

// Input
cn("px-4 py-2", "px-6")
// Output: "py-2 px-6" (px-6 wins, py-2 preserved)

// Input
cn("text-red-500", "text-blue-500")
// Output: "text-blue-500" (last color wins)

// Input
cn("hover:bg-red-500", "hover:bg-blue-500")
// Output: "hover:bg-blue-500" (handles modifiers correctly)

Common Patterns

Component Variants

const buttonVariants = {
  primary: "bg-blue-500 text-white hover:bg-blue-600",
  secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",
  ghost: "bg-transparent hover:bg-gray-100",
};

const buttonSizes = {
  sm: "px-3 py-1.5 text-sm",
  md: "px-4 py-2 text-base",
  lg: "px-6 py-3 text-lg",
};

function Button({ variant = "primary", size = "md", className, ...props }) {
  return (
    <button
      className={cn(
        "rounded-md font-medium transition-colors",
        buttonVariants[variant],
        buttonSizes[size],
        className
      )}
      {...props}
    />
  );
}

Forwarding className

Always accept and forward className as the last argument to cn():

// Correct: className last, can override anything
function Card({ className, ...props }) {
  return (
    <div
      className={cn(
        "rounded-lg border p-4 shadow-sm",
        className  // Consumer can override any default
      )}
      {...props}
    />
  );
}

// Usage
<Card className="p-8 shadow-lg" />
// Result: p-8 and shadow-lg override defaults

With cva (Class Variance Authority)

cn() pairs well with cva for complex variant systems:

import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md font-medium",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        outline: "border border-input bg-background hover:bg-accent",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 px-3",
        lg: "h-11 px-8",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {}

function Button({ className, variant, size, ...props }: ButtonProps) {
  return (
    <button
      className={cn(buttonVariants({ variant, size }), className)}
      {...props}
    />
  );
}

TypeScript

The cn() function is fully typed:

import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

// ClassValue accepts:
// - string
// - number
// - boolean | null | undefined (falsy values ignored)
// - ClassValue[] (arrays)
// - { [key: string]: boolean } (objects)

Animation Constants

Several SmoothUI components share a small set of animation constants — spring configs, easing curves, and durations — so motion feels consistent across the library. They live in lib/animation.ts and follow the project's animation guidelines (durations of 0.2–0.25s and bounce ≤ 0.1 for standard UI).

Installation

You don't install these directly. Any component that uses them (for example pagination, select, combobox, dropdown-menu, context-menu, radio-group, checkbox, form, or breadcrumb) lists them as a registry dependency, so shadcn pulls lib/animation.ts into your project automatically:

npx shadcn@latest add @smoothui/pagination

The file lands at components/smoothui/lib/animation.ts and the component's imports resolve to @/components/smoothui/lib/animation. To add it on its own:

npx shadcn@latest add @smoothui/lib

Available Constants

// components/smoothui/lib/animation.ts

/** Default spring for most UI animations */
export const SPRING_DEFAULT = { type: "spring", duration: 0.25, bounce: 0.1 };

/** Snappy spring for quick, no-overshoot transitions */
export const SPRING_SNAPPY = { type: "spring", duration: 0.2, bounce: 0 };

/** Ease-out for entering elements — cubic-bezier(.23, 1, .32, 1) */
export const EASE_OUT = [0.23, 1, 0.32, 1] as const;

/** Ease-in-out for moving elements — cubic-bezier(0.645, 0.045, 0.355, 1) */
export const EASE_IN_OUT = [0.645, 0.045, 0.355, 1] as const;

/** Instant transition for reduced-motion contexts */
export const DURATION_INSTANT = { duration: 0 };

/** Standard animation durations (seconds) */
export const DURATION = { fast: 0.15, default: 0.25, slow: 0.3, complex: 0.4 } as const;

Usage

Feed them straight into Motion's transition prop, and swap to DURATION_INSTANT when the user prefers reduced motion:

import { motion, useReducedMotion } from "motion/react";
import { SPRING_DEFAULT, DURATION_INSTANT } from "@/components/smoothui/lib/animation";

function Panel() {
  const shouldReduceMotion = useReducedMotion();

  return (
    <motion.div
      animate={{ opacity: 1, y: 0 }}
      initial={{ opacity: 0, y: 10 }}
      transition={shouldReduceMotion ? DURATION_INSTANT : SPRING_DEFAULT}
    />
  );
}

Use the easing curves anywhere Motion accepts an ease value:

import { EASE_OUT } from "@/components/smoothui/lib/animation";

<motion.div
  animate={{ opacity: 1 }}
  transition={{ duration: DURATION.default, ease: EASE_OUT }}
/>

Best Practices

Always Use cn() for Component Styling

// Good: Allows overrides, resolves conflicts
className={cn("base-styles", className)}

// Avoid: No conflict resolution
className={`base-styles ${className}`}

Keep Base Styles First

cn(
  "base-styles",      // 1. Foundational styles
  "variant-styles",   // 2. Variant-specific
  "state-styles",     // 3. State-based (hover, active)
  className           // 4. Consumer overrides (last!)
)

Use Semantic Groups

cn(
  // Layout
  "flex items-center gap-2",
  // Sizing
  "h-10 px-4",
  // Typography
  "text-sm font-medium",
  // Colors
  "bg-blue-500 text-white",
  // Interactions
  "hover:bg-blue-600 focus:ring-2",
  // Overrides
  className
)

On this page