Docs
Utility helpers keep className composition predictable when variants, state, and Tailwind strings collide. Nexus UI uses clsx plus tailwind-merge behind a single cn() export — and Motion for all animation.
Install the three libraries every Nexus UI component relies on: clsx for conditional class logic, tailwind-merge for conflict resolution, and motion for animations.
npm install clsx tailwind-merge motionIf you prefer the original Framer Motion package name, both are compatible — Nexus UI components import from motion/react (the Motion library):
npm install clsx tailwind-merge framer-motionCreate src/lib/utils.ts (this exact file is used throughout Nexus UI):
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}The cn() function accepts any mix of strings, arrays, and objects (the full clsx API), then runs the result through twMerge so conflicting Tailwind utilities are resolved by last-argument-wins semantics.
Pass static base classes alongside conditional prop-driven fragments — the helper keeps the final string clean:
import { cn } from "@/lib/utils";
function Card({ className, active }: { className?: string; active?: boolean }) {
return (
<div
className={cn(
// base styles
"rounded-xl border border-border bg-card p-4",
// conditional variant
active && "border-primary ring-2 ring-primary/30",
// consumer override wins
className,
)}
/>
);
}Tailwind classes are resolved by the order they appear in your CSS output, not the order they appear in the className string. Concatenating two conflicting utilities — e.g. p-4 p-6 — produces unpredictable results because whichever class was declared last in the stylesheet wins, regardless of string order.
twMergeunderstands Tailwind's utility groups and strips any earlier arguments that conflict with later ones, so design-system defaults and consumer overrides always compose safely:
// Without twMerge — unpredictable winner
"p-4 p-6" // both classes in DOM; CSS order decides
// With twMerge — later argument always wins
cn("p-4", "p-6") // → "p-6"
cn("p-4", props.className) // consumer override is respectedIf you are using Next.js 15 with React 19 and see peer-dependency warnings from the motion package, add an overrides block to your package.json:
{
"overrides": {
"motion": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
}If you are using framer-motion instead of motion, replace the key accordingly:
{
"overrides": {
"framer-motion": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
}Run npm install again after editing package.json.
Import cn in any component and check the output. With TypeScript the function signature is:
import { cn } from "@/lib/utils";
// resolves to "rounded-xl bg-primary text-white px-6"
const cls = cn("rounded-xl bg-muted", "bg-primary text-white", "px-4 px-6");
console.log(cls);No TypeScript errors and the output string should contain only px-6 (not px-4) and bg-primary (not bg-muted). If the imports resolve correctly your utility layer is ready.