classnames, clsx, and tailwind-merge are utility libraries designed to help developers compose CSS class strings dynamically in JavaScript applications. They address the common challenge of conditionally applying classes based on component state, props, or logic, but differ significantly in scope, performance characteristics, and intended use cases — especially when working with utility-first CSS frameworks like Tailwind CSS.
When building dynamic UIs, we often need to toggle or combine CSS classes based on state, props, or user interactions. All three packages solve this problem — but they operate at different layers of abstraction and make different trade-offs. Let’s break down how they work and where each shines.
classnames and clsx do the same thing: they take a mix of strings, arrays, and objects and return a clean space-separated class string. Truthy values are included; falsy ones are skipped.
// Both produce: "btn btn-primary active"
// classnames
import classNames from 'classnames';
classNames('btn', 'btn-primary', { active: true });
// clsx
import clsx from 'clsx';
clsx('btn', 'btn-primary', { active: true });
They treat all inputs as opaque strings — no understanding of what the classes mean. If you pass conflicting classes like text-red-500 text-blue-500, both will output both, and CSS specificity (or source order) decides the winner.
tailwind-merge, by contrast, understands Tailwind’s design system. It parses incoming classes, identifies conflicts within the same category (e.g., two text colors), and keeps only the last one — mimicking how developers expect overrides to work.
import { twMerge } from 'tailwind-merge';
// Returns: "px-4 py-2 text-blue-500"
// (drops text-red-500 because text-blue-500 comes later)
twMerge('px-4 py-2 text-red-500', 'text-blue-500');
This behavior is impossible for classnames or clsx because they don’t know that text-red-500 and text-blue-500 belong to the same logical group.
All three are fast, but their cost profiles differ:
clsx is the leanest. It uses minimal logic and avoids function calls where possible. In tight loops (e.g., rendering 1,000 list items), it edges out classnames.classnames has slightly more overhead due to its support for older patterns (like passing an array as the first argument). This rarely matters in practice but shows up in microbenchmarks.tailwind-merge is heavier because it must parse and categorize every class. However, it caches parsed results internally, so repeated calls with the same inputs are fast. Still, avoid using it inside render functions with highly dynamic, unique class combinations.💡 Pro tip: If you’re using
tailwind-merge, wrap it in a memoized helper when used in React components:const buttonClass = useMemo( () => twMerge('px-4 py-2', variant === 'primary' ? 'bg-blue-500' : 'bg-gray-300'), [variant] );
You’re building a reusable <Button> that supports disabled, loading, and variant props.
✅ Use clsx (or classnames):
function Button({ disabled, loading, variant, className }) {
return (
<button
className={clsx(
'px-4 py-2 rounded',
{
'bg-blue-500': variant === 'primary',
'bg-gray-300': variant === 'secondary',
'opacity-50 cursor-not-allowed': disabled || loading
},
className
)}
/>
);
}
No class conflicts here — just additive modifiers. Overkill to involve tailwind-merge.
You accept a className prop that might override base styles, e.g., <Card className="border-red-500 bg-white" />.
✅ Use tailwind-merge:
function Card({ className }) {
return (
<div
className={twMerge(
'border border-gray-200 bg-gray-50 p-4', // base
className // might include border-red-500 → should win
)}
/>
);
}
Without tailwind-merge, both border-gray-200 and border-red-500 would appear, and the visual result would depend on CSS source order — often leading to bugs when consumers try to customize components.
Your app uses Bootstrap and Tailwind (yes, it happens during migrations).
⚠️ None of these libraries fully solve this. tailwind-merge only understands Tailwind classes — it leaves non-Tailwind classes untouched. So:
twMerge('btn btn-primary text-red-500', 'text-blue-500');
// → "btn btn-primary text-blue-500"
The Bootstrap classes (btn, btn-primary) are preserved, and Tailwind conflict resolution works as expected. But if you had conflicting Bootstrap classes (btn-primary btn-secondary), tailwind-merge wouldn’t touch them. In hybrid setups, you may need to layer strategies — e.g., use clsx for framework classes and twMerge only on the Tailwind subset.
clsx plays nicely with any styling approach: CSS Modules, styled-components, vanilla CSS, or Tailwind (as long as you manage conflicts manually).tailwind-merge is purpose-built for Tailwind. It integrates smoothly with component libraries like shadcn/ui and is often paired with clsx:import clsx from 'clsx';
import { twMerge } from 'tailwind-merge';
// Common pattern in modern Tailwind codebases
const cn = (...inputs) => twMerge(clsx(inputs));
// Now use `cn` everywhere
<div className={cn('p-4', isError && 'border-red-500')} />
This combo gives you the ergonomic input handling of clsx and the smart merging of tailwind-merge.
classnames in new projects unless you have a specific compatibility requirement. clsx is a drop-in replacement with better optimization.clsx alone in a Tailwind project where consumers pass arbitrary class overrides — you’ll get unpredictable styling.tailwind-merge for non-Tailwind classes. It adds unnecessary overhead and won’t resolve conflicts in other systems.clsx.tailwind-merge, ideally wrapped with clsx via a cn helper.classnames for legacy maintenance.These tools aren’t competitors — they solve different problems. The real pro move is knowing which layer of your styling stack needs which tool.
Choose clsx if you want a lightweight, modern alternative to classnames with identical functionality but better tree-shaking support and slightly improved runtime performance. It’s ideal for new React, Vue, or Svelte projects where bundle size matters and you’re not using Tailwind CSS — or only using it without complex layering or conflicting utilities.
Choose classnames if you're maintaining a legacy codebase that already depends on it or if you need maximum compatibility across environments (including older browsers and non-module systems). Its API is stable and well-understood by most frontend teams, making it a safe default for general-purpose class composition outside of Tailwind-heavy projects.
Choose tailwind-merge if your project uses Tailwind CSS extensively and you frequently run into class conflicts due to conditional logic (e.g., text-red-500 vs text-blue-500). It intelligently merges and overrides Tailwind classes so the final output respects the expected visual outcome, which neither classnames nor clsx can do.
A tiny (239B) utility for constructing
classNamestrings conditionally.
Also serves as a faster & smaller drop-in replacement for theclassnamesmodule.
This module is available in three formats:
dist/clsx.mjsdist/clsx.jsdist/clsx.min.js$ npm install --save clsx
import clsx from 'clsx';
// or
import { clsx } from 'clsx';
// Strings (variadic)
clsx('foo', true && 'bar', 'baz');
//=> 'foo bar baz'
// Objects
clsx({ foo:true, bar:false, baz:isTrue() });
//=> 'foo baz'
// Objects (variadic)
clsx({ foo:true }, { bar:false }, null, { '--foobar':'hello' });
//=> 'foo --foobar'
// Arrays
clsx(['foo', 0, false, 'bar']);
//=> 'foo bar'
// Arrays (variadic)
clsx(['foo'], ['', 0, false, 'bar'], [['baz', [['hello'], 'there']]]);
//=> 'foo bar baz hello there'
// Kitchen sink (with nesting)
clsx('foo', [1 && 'bar', { baz:false, bat:null }, ['hello', ['world']]], 'cya');
//=> 'foo bar hello world cya'
Returns: String
Type: Mixed
The clsx function can take any number of arguments, each of which can be an Object, Array, Boolean, or String.
Important: Any falsey values are discarded!
Standalone Boolean values are discarded as well.
clsx(true, false, '', null, undefined, 0, NaN);
//=> ''
There are multiple "versions" of clsx available, which allows you to bring only the functionality you need!
clsxSize (gzip): 239 bytes
Availability: CommonJS, ES Module, UMD
The default clsx module; see API for info.
import { clsx } from 'clsx';
// or
import clsx from 'clsx';
clsx/liteSize (gzip): 140 bytes
Availability: CommonJS, ES Module
CAUTION: Accepts ONLY string arguments!
Ideal for applications that only use the string-builder pattern.
Any non-string arguments are ignored!
import { clsx } from 'clsx/lite';
// or
import clsx from 'clsx/lite';
// string
clsx('hello', true && 'foo', false && 'bar');
// => "hello foo"
// NOTE: Any non-string input(s) ignored
clsx({ foo: true });
//=> ""
For snapshots of cross-browser results, check out the bench directory~!
All versions of Node.js are supported.
All browsers that support Array.isArray are supported (IE9+).
Note: For IE8 support and older, please install
clsx@1.0.xand beware of #17.
Here some additional (optional) steps to enable classes autocompletion using clsx with Tailwind CSS.
Install the "Tailwind CSS IntelliSense" Visual Studio Code extension
Add the following to your settings.json:
{
"tailwindCSS.experimental.classRegex": [
["clsx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
]
}
You may find the clsx/lite module useful within Tailwind contexts. This is especially true if/when your application only composes classes in this pattern:
clsx('text-base', props.active && 'text-primary', props.className);
MIT © Luke Edwards