Skip to main content

cx

A class name composition utility that combines conditional class names and resolves Tailwind CSS conflicts using left-to-right merge order.

Powered by a vendored, byte-for-byte port of clsx (conditional class handling) and tailwind-merge (conflict resolution), adapted from cnfast. Later classes override earlier ones (same behavior as inline styles). Mantle's Tailwind overrides, the em spacing scale (w-em, p-em, …) and the text-mono / text-size-inherit font-size utilities, are baked in, so neither clsx nor tailwind-merge is a runtime dependency.

import { cx } from "@ngrok/mantle/cx";

Usage

import { cx } from "@ngrok/mantle/cx"; // Stringscx("px-4 py-2", "text-sm");// → "px-4 py-2 text-sm" // Conditional classes (object syntax)cx("base", { active: isActive, disabled: isDisabled }); // Conditional classes (falsy values are ignored)cx("base", isActive && "active", isDisabled && "disabled"); // Tailwind conflict resolution — later class winscx("text-red-500", "text-blue-500");// → "text-blue-500" // Arrayscx(["px-4", "py-2"], "text-sm");

Tailwind conflict resolution

cx uses tailwind-merge to resolve conflicts between Tailwind utility classes. When two classes target the same CSS property, the last one wins:

// Without cx — both classes are applied, browser resolves by specificity (unpredictable, stinky)<div className={`text-red-500 ${override}`} /> // With cx — later class always wins (predictable, like inline styles)<div className={cx("text-red-500", override)} />

This makes cx safe to use for component prop overrides:

import { cx } from "@ngrok/mantle/cx"; type ButtonProps = {	className?: string;}; function Button({ className }: ButtonProps) {	return <button className={cx("bg-blue-500 text-white px-4 py-2 rounded", className)} />;} // The consumer's class overrides the default<Button className="bg-red-500" />;// → "text-white px-4 py-2 rounded bg-red-500"

Tagged template

cx can also be called as a tagged template. A tagged template reuses the same static strings array on every render, so cx caches the result by call-site identity, useful for hot re-rendering paths. The behavior is otherwise identical to the call form.

cx`px-2 px-4 ${isActive && "bg-blue-500"}`;// → "px-4 bg-blue-500"

API

function cx(...inputs: ClassValue[]): string;// also callable as a tagged template: cx`px-2 ${cond && "px-4"}` // ClassValue, looks like this 👇type ClassValue =	ClassArray | ClassDictionary | string | number | bigint | null | boolean | undefined;type ClassDictionary = Record<string, any>;type ClassArray = ClassValue[];

Accepts any combination of:

Input typeExampleNotes
string"px-4 py-2"Added as-is
object{ active: true, disabled: false }Keys with truthy values are included
array["px-4", isActive && "bg-blue-500"]Recursively processed
Falsy valuesfalse, null, undefined, 0, ""Ignored

The return value is a single deduplicated, conflict-resolved class string.

Convention

All className values in Mantle must be built with cx. String interpolation, +, ternaries, or conditional expressions directly on className are not allowed; see CONVENTIONS.md.