classnames and clsx are both lightweight utility libraries designed to help developers construct conditional CSS class strings in JavaScript-based UIs. They solve the common problem of dynamically combining static and conditional classes without manual string concatenation or complex ternary logic. Both accept a mix of strings, objects, arrays, and nested structures, returning a clean space-delimited class name string suitable for use in React, Vue, or vanilla DOM APIs.
When building dynamic user interfaces, we often need to toggle CSS classes based on component state, props, or application context. Writing inline conditionals like ${isActive ? 'active' : ''} ${isLoading ? 'loading' : ''} quickly becomes messy and error-prone. That’s where classnames and clsx come in — both provide clean, declarative APIs to compose class strings safely. But under the hood, they differ in philosophy, scope, and performance characteristics.
Both libraries accept the same basic input types:
// Shared behavior example
const isActive = true;
const isLoading = false;
// Both return "btn primary active"
classnames('btn', 'primary', { active: isActive, loading: isLoading });
clsx('btn', 'primary', { active: isActive, loading: isLoading });
However, their treatment of edge cases diverges.
classnames explicitly filters out all falsy values (false, null, undefined, 0, NaN, empty string):
// Returns "visible" — skips 0 and empty string
classnames('visible', 0, '', null, undefined);
clsx behaves identically here — both treat these as non-renderable and omit them.
classnames fully flattens arbitrarily deep arrays:
// Returns "a b c d"
classnames(['a', ['b', ['c']], 'd']);
clsx only flattens one level deep by design:
// Returns "a b,c d" — inner array becomes "b,c" via .join(' ')
clsx(['a', ['b', 'c'], 'd']);
In practice, this rarely matters because most developers avoid deeply nested class arrays. But if your codebase uses recursive component composition that generates nested structures, classnames is more forgiving.
clsx is optimized for the 95% case: flat inputs with strings and simple objects. It avoids recursion and uses tight loops, making it consistently faster in benchmarks involving typical React prop patterns.
classnames, while still fast, carries slight overhead from its robust flattening logic and broader type handling. This difference is negligible in most apps but can add up in:
If you’re building a dashboard with 10k+ dynamic table rows, clsx might shave off meaningful milliseconds.
Both packages are tiny, but clsx has a simpler internal structure that plays better with modern bundlers. Its ESM build exports a single function with no side effects, enabling perfect tree-shaking even in complex dependency graphs.
classnames also supports ESM, but its historical UMD roots mean some older tooling configurations might pull in slightly more dead code. In contemporary setups (Vite, modern Webpack/Rollup), this gap has mostly closed.
Both offer identical DX for standard usage. However, when things go wrong:
classnames’s exhaustive flattening can mask bugs caused by accidentally passing nested arraysclsx’s stricter approach surfaces structural issues earlier (e.g., seeing "b,c" instead of "b c" reveals unintended nesting)Consider this debugging scenario:
// Accidentally double-wrapped array
const extraClasses = [['highlight']];
// classnames: silently works → "btn highlight"
// clsx: exposes mistake → "btn highlight" (wait, why isn't it working?)
// Actually returns "btn highlight" in both? Let's correct:
// clsx(['btn', extraClasses]) → "btn highlight" (same as classnames)
// Correction: The real difference appears with deeper nesting:
const badInput = ['btn', [['error']]];
// classnames → "btn error"
// clsx → "btn error" (still same? Actually...)
Upon closer inspection, the practical debugging difference is minimal because both handle two-level nesting correctly. The divergence only appears at three+ levels, which is uncommon. So this point is often overstated in discussions.
The APIs are so similar that switching between them usually requires only a find-and-replace of the import statement:
- import classNames from 'classnames';
+ import clsx from 'clsx';
- const className = classNames('foo', { bar: true });
+ const className = clsx('foo', { bar: true });
This near-perfect compatibility means you can evaluate either without major refactoring risk.
clsx when:classnames when:classnames-style deep flatteningFor new greenfield projects, clsx is generally the better choice — it’s faster, smaller, and sufficient for virtually all real-world use cases. Its constraints encourage cleaner data structures.
For existing applications, stick with whichever you already use unless you have specific performance bottlenecks tied to class composition. The migration effort rarely justifies the marginal gains.
Most importantly: don’t overthink it. Both solve the core problem elegantly, and your team’s consistency matters more than micro-optimizations. Pick one, enforce it via lint rules, and move on to harder problems.
Choose classnames if you're working on a mature codebase that already depends on it or if your team prefers a battle-tested solution with broad adoption across legacy and modern projects. Its API handles edge cases predictably and supports deeply nested arrays and mixed-type inputs reliably, making it a safe default for applications where stability trumps minimalism.
Choose clsx if you prioritize a smaller runtime footprint and slightly faster execution in performance-sensitive contexts like high-frequency renders or server-side rendering at scale. Its implementation is more streamlined and optimized for the most common usage patterns (strings, flat objects, and shallow arrays), which covers 95%+ of real-world scenarios while avoiding overhead from handling rare edge cases.
A simple JavaScript utility for conditionally joining classNames together.
Install from the npm registry with your package manager:
npm install classnames
Use with Node.js, Browserify, or webpack:
const classNames = require('classnames');
classNames('foo', 'bar'); // => 'foo bar'
Alternatively, you can simply include index.js on your page with a standalone <script> tag and it will export a global classNames method, or define the module if you are using RequireJS.
We take the stability and performance of this package seriously, because it is run millions of times a day in browsers all around the world. Updates are thoroughly reviewed for performance implications before being released, and we have a comprehensive test suite.
Classnames follows the SemVer standard for versioning.
There is also a Changelog.
The classNames function takes any number of arguments which can be a string or object.
The argument 'foo' is short for { foo: true }. If the value associated with a given key is falsy, that key won't be included in the output.
classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); // => ''
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'
// lots of arguments of various types
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'
// other falsy values are just ignored
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'
Arrays will be recursively flattened as per the rules above:
const arr = ['b', { c: true, d: false }];
classNames('a', arr); // => 'a b c'
If you're in an environment that supports computed keys (available in ES2015 and Babel) you can use dynamic class names:
let buttonType = 'primary';
classNames({ [`btn-${buttonType}`]: true });
This package is the official replacement for classSet, which was originally shipped in the React.js Addons bundle.
One of its primary use cases is to make dynamic and conditional className props simpler to work with (especially more so than conditional string manipulation). So where you may have the following code to generate a className prop for a <button> in React:
import React, { useState } from 'react';
export default function Button (props) {
const [isPressed, setIsPressed] = useState(false);
const [isHovered, setIsHovered] = useState(false);
let btnClass = 'btn';
if (isPressed) btnClass += ' btn-pressed';
else if (isHovered) btnClass += ' btn-over';
return (
<button
className={btnClass}
onMouseDown={() => setIsPressed(true)}
onMouseUp={() => setIsPressed(false)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{props.label}
</button>
);
}
You can express the conditional classes more simply as an object:
import React, { useState } from 'react';
import classNames from 'classnames';
export default function Button (props) {
const [isPressed, setIsPressed] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const btnClass = classNames({
btn: true,
'btn-pressed': isPressed,
'btn-over': !isPressed && isHovered,
});
return (
<button
className={btnClass}
onMouseDown={() => setIsPressed(true)}
onMouseUp={() => setIsPressed(false)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{props.label}
</button>
);
}
Because you can mix together object, array and string arguments, supporting optional className props is also simpler as only truthy arguments get included in the result:
const btnClass = classNames('btn', this.props.className, {
'btn-pressed': isPressed,
'btn-over': !isPressed && isHovered,
});
dedupe versionThere is an alternate version of classNames available which correctly dedupes classes and ensures that falsy classes specified in later arguments are excluded from the result set.
This version is slower (about 5x) so it is offered as an opt-in.
To use the dedupe version with Node.js, Browserify, or webpack:
const classNames = require('classnames/dedupe');
classNames('foo', 'foo', 'bar'); // => 'foo bar'
classNames('foo', { foo: false, bar: true }); // => 'bar'
For standalone (global / AMD) use, include dedupe.js in a <script> tag on your page.
bind version (for css-modules)If you are using css-modules, or a similar approach to abstract class 'names' and the real className values that are actually output to the DOM, you may want to use the bind variant.
Note that in ES2015 environments, it may be better to use the "dynamic class names" approach documented above.
const classNames = require('classnames/bind');
const styles = {
foo: 'abc',
bar: 'def',
baz: 'xyz',
};
const cx = classNames.bind(styles);
const className = cx('foo', ['bar'], { baz: true }); // => 'abc def xyz'
Real-world example:
/* components/submit-button.js */
import { useState } from 'react';
import classNames from 'classnames/bind';
import styles from './submit-button.css';
const cx = classNames.bind(styles);
export default function SubmitButton ({ store, form }) {
const [submissionInProgress, setSubmissionInProgress] = useState(store.submissionInProgress);
const [errorOccurred, setErrorOccurred] = useState(store.errorOccurred);
const [valid, setValid] = useState(form.valid);
const text = submissionInProgress ? 'Processing...' : 'Submit';
const className = cx({
base: true,
inProgress: submissionInProgress,
error: errorOccurred,
disabled: valid,
});
return <button className={className}>{text}</button>;
}
classNames >=2.0.0Array.isArray: see MDN for details about unsupported older browsers (e.g. <= IE8) and a simple polyfill.
Copyright (c) 2018 Jed Watson. Copyright of the Typescript bindings are respective of each contributor listed in the definition file.