The class name utility that does what
clsxcanβt.
Production-ready, TypeScript-first, with built-in Tailwind merge and variant composition.
Youβve been using clsx, classnames, or classcat for years. Theyβre great! But hereβs what they canβt do:
// β With clsx - you need TWO libraries
import clsx from "clsx";
import { twMerge } from "tailwind-merge";
const className = twMerge(clsx("px-4 py-2", "px-6")); // 'py-2 px-6'
// β
With @cx-utils/core - ONE library, built-in
import { mergeCx } from "@cx-utils/core";
const className = mergeCx("px-4 py-2", "px-6"); // 'py-2 px-6'
Plus, you get type-safe variant composition (like CVA) included:
// β With clsx - need class-variance-authority too
import clsx from "clsx";
import { cva } from "class-variance-authority";
// β
With @cx-utils/core - built-in!
import { composeClasses } from "@cx-utils/core";
const button = composeClasses({
base: "px-4 py-2 rounded",
variants: {
color: { primary: "bg-blue-500", secondary: "bg-gray-500" },
size: { sm: "text-sm", lg: "text-lg" },
},
});
cx() - Drop-in replacement for clsx/classnames/classcatmergeCx() - Built-in Tailwind CSS conflict resolution (no tailwind-merge needed!)composeClasses() - Type-safe variants (no class-variance-authority needed!)any types - Strong generics throughout| Feature | @cx-utils/core | clsx | classnames | classcat |
|---|---|---|---|---|
| Basic class merging | β | β | β | β |
| TypeScript generics | β Strong | β οΈ Basic | β | β οΈ Basic |
| Tailwind merge | β Built-in | β Need tailwind-merge |
β | β |
| Variant composition | β Built-in | β Need CVA | β | β |
| Tree-shakable | β | β | β οΈ | β |
| Bundle size | π’ ~3KB | π’ ~1KB | π‘ ~2KB | π’ ~1KB |
| Dependencies | β Zero | β Zero | β Zero | β Zero |
| Tailwind v4 ready | β | β | β | β |
| Active maintenance | β 2025 | β 2024 | β οΈ 2021 | β οΈ 2020 |
Using clsx + tailwind-merge + CVA:
{
"dependencies": {
"clsx": "^2.0.0", // 1KB
"tailwind-merge": "^2.0.0", // 15KB
"class-variance-authority": "^0.7.0" // 5KB
}
}
// Total: ~21KB + 3 dependencies
Using @cx-utils/core:
{
"dependencies": {
"@cx-utils/core": "^1.0.0" // 3KB
}
}
// Total: ~3KB + 0 dependencies β¨
tailwind-merge functionalitynpm install @cx-utils/core
yarn add @cx-utils/core
pnpm add @cx-utils/core
import { cx, mergeCx, composeClasses } from "@cx-utils/core";
// Basic usage
cx("foo", "bar"); // 'foo bar'
cx("foo", false, "bar"); // 'foo bar'
cx({ foo: true, bar: false }); // 'foo'
cx(["foo", "bar"], "baz"); // 'foo bar baz'
// Tailwind merge
mergeCx("px-4 py-2", "px-6"); // 'py-2 px-6'
// Variant composition
const button = composeClasses({
base: "px-4 py-2 rounded",
variants: {
color: {
primary: "bg-blue-500 text-white",
secondary: "bg-gray-500 text-white",
},
},
});
button({ color: "primary" }); // 'px-4 py-2 rounded bg-blue-500 text-white'
cx(...inputs: ClassValue[]): stringCombines class names into a single string. Accepts strings, numbers, arrays, objects, and nested structures.
Filters out falsy values: false, null, undefined, 0, ""
// Strings
cx("foo", "bar", "baz");
// β 'foo bar baz'
// Objects (conditional classes)
cx({ foo: true, bar: false, baz: true });
// β 'foo baz'
// Arrays
cx(["foo", "bar"], "baz");
// β 'foo bar baz'
// Nested arrays
cx("base", ["foo", { bar: true }], [["nested"]]);
// β 'base foo bar nested'
// Mixed inputs
cx("btn", { active: isActive, disabled: isDisabled }, ["rounded", "shadow"]);
// β 'btn active rounded shadow' (if isActive=true, isDisabled=false)
mergeCx(...inputs: ClassValue[]): stringCombines class names with Tailwind CSS conflict resolution. Later classes override earlier conflicting classes.
// Padding conflicts
mergeCx("px-4 py-2", "px-6");
// β 'py-2 px-6'
// Text size conflicts
mergeCx("text-sm text-blue-500", "text-lg");
// β 'text-blue-500 text-lg'
// Background conflicts
mergeCx("bg-red-500", "bg-blue-500");
// β 'bg-blue-500'
// Responsive variants
mergeCx("px-4 md:px-6", "lg:px-8");
// β 'px-4 md:px-6 lg:px-8'
// State variants
mergeCx("hover:bg-red-500", "hover:bg-blue-500");
// β 'hover:bg-blue-500'
composeClasses(config: VariantConfig): (props?: VariantProps) => stringCreates a type-safe variant composer for building component APIs with variants.
const button = composeClasses({
base: "inline-flex items-center justify-center rounded-md font-medium",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground",
outline: "border border-input bg-background hover:bg-accent",
ghost: "hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 px-3",
lg: "h-11 px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
compoundVariants: [
{
variant: "destructive",
size: "lg",
class: "font-bold",
},
],
});
// Usage
button(); // Uses default variants
button({ variant: "outline", size: "sm" });
button({ variant: "destructive", size: "lg" }); // Includes compound variant
button({ variant: "ghost", className: "mt-4" }); // Add custom classes
splitClasses(classString: string): string[]Splits a class string into an array of individual class names.
splitClasses("foo bar baz");
// β ['foo', 'bar', 'baz']
isTruthyClass(value: unknown): booleanType guard that checks if a value should be included as a class name.
isTruthyClass("foo"); // true
isTruthyClass(false); // false
isTruthyClass(0); // false
isTruthyClass(null); // false
import { cx, mergeCx } from "@cx-utils/core";
function Button({ isActive, isDisabled, children }) {
return (
<button
className={cx("px-4 py-2 rounded font-medium", {
"bg-blue-500 text-white": isActive,
"bg-gray-300 text-gray-700": !isActive,
"opacity-50 cursor-not-allowed": isDisabled,
})}
>
{children}
</button>
);
}
function Card({ className, children }) {
return (
<div
className={mergeCx(
"p-4 bg-white rounded-lg shadow",
className // User overrides
)}
>
{children}
</div>
);
}
// Usage: <Card className="p-6 bg-gray-100">...</Card>
// Result: "rounded-lg shadow p-6 bg-gray-100"
"use client";
import { composeClasses } from "@cx-utils/core";
const buttonVariants = composeClasses({
base: "inline-flex items-center justify-center rounded-md font-medium transition-colors",
variants: {
variant: {
default: "bg-blue-600 text-white hover:bg-blue-700",
outline: "border-2 border-blue-600 text-blue-600 hover:bg-blue-50",
},
size: {
sm: "h-9 px-3 text-sm",
md: "h-10 px-4",
lg: "h-12 px-6 text-lg",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
});
export function Button({ variant, size, className, children, ...props }) {
return (
<button className={buttonVariants({ variant, size, className })} {...props}>
{children}
</button>
);
}
<script type="module">
import { cx, mergeCx } from "@cx-utils/core";
const button = document.createElement("button");
button.className = cx("px-4 py-2 rounded", { active: true, disabled: false });
</script>
<script type="module">
import { cx } from "https://unpkg.com/@cx-utils/core/dist/index.mjs";
console.log(cx("foo", "bar")); // 'foo bar'
</script>
clsx / classnames| Feature | @cx-utils/core | clsx | classnames |
|---|---|---|---|
| Zero dependencies | β | β | β |
| TypeScript generics | β | β οΈ | β |
| Tailwind merge | β | β | β |
| Variant composition | β | β | β |
| Tree-shakable | β | β | β οΈ |
| Performance | β‘ Fast | β‘ Fast | π’ Slower |
Based on our benchmarks (see npm run bench):
const alert = composeClasses({
base: "p-4 rounded-lg border",
variants: {
type: {
error: "bg-red-50 border-red-200 text-red-800",
warning: "bg-yellow-50 border-yellow-200 text-yellow-800",
info: "bg-blue-50 border-blue-200 text-blue-800",
success: "bg-green-50 border-green-200 text-green-800",
},
size: {
sm: "text-sm p-2",
md: "text-base p-4",
lg: "text-lg p-6",
},
},
defaultVariants: {
type: "info",
size: "md",
},
compoundVariants: [
{
type: "error",
size: "lg",
class: "font-bold shadow-lg",
},
],
});
function ComplexComponent({ isPrimary, isLarge, isActive, hasError }) {
return (
<div
className={cx(
"component-base",
[
"rounded shadow",
{
"bg-blue-500": isPrimary,
"bg-gray-500": !isPrimary,
},
],
[
isLarge ? ["text-lg", "p-6"] : ["text-sm", "p-4"],
{
"ring-2 ring-blue-400": isActive,
"border-2 border-red-500": hasError,
},
]
)}
/>
);
}
clsx// Before (clsx)
import clsx from "clsx";
const className = clsx("foo", { bar: true });
// After (@cx-utils/core)
import { cx } from "@cx-utils/core";
const className = cx("foo", { bar: true });
classnames// Before (classnames)
import classNames from "classnames";
const className = classNames("foo", { bar: true });
// After (@cx-utils/core)
import { cx } from "@cx-utils/core";
const className = cx("foo", { bar: true });
tailwind-merge// Before (tailwind-merge + clsx)
import { twMerge } from "tailwind-merge";
import clsx from "clsx";
const className = twMerge(clsx("px-4", "px-6"));
// After (@cx-utils/core)
import { mergeCx } from "@cx-utils/core";
const className = mergeCx("px-4", "px-6");
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Run performance tests
npm run test:perf
npm run bench
# Install dependencies
npm install
# Build
npm run build
# Type check
npm run typecheck
# Run all checks before publishing
npm run prepublishOnly
cx-utils is MIT licensed.
Contributions are welcome! Please feel free to submit a Pull Request.
Made with β€οΈ for the React and Tailwind CSS community