clsx vs classnames
Conditional CSS Class Composition in JavaScript Applications
clsxclassnamesSimilar Packages:
Conditional CSS Class Composition in JavaScript Applications

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.

Npm Package Weekly Downloads Trend
3 Years
Github Stars Ranking
Stat Detail
Package
Downloads
Stars
Size
Issues
Publish
License
clsx38,990,4069,5218.55 kB142 years agoMIT
classnames18,979,75017,79223.6 kB102 years agoMIT

classnames vs clsx: A Deep Dive for Frontend Engineers

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.

🧩 Core Behavior: What Gets Included?

Both libraries accept the same basic input types:

  • Strings: always included
  • Objects: keys included if value is truthy
  • Arrays: recursively flattened
// 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.

Handling Falsy Values

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.

Nested Arrays and Deep Structures

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.

⚡ Performance Characteristics

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:

  • Large lists with frequent re-renders
  • Server-rendered pages generating thousands of elements
  • Animation-heavy components updating classes every frame

If you’re building a dashboard with 10k+ dynamic table rows, clsx might shave off meaningful milliseconds.

📦 Bundle Impact and Tree-Shaking

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.

💬 Developer Experience and Debugging

Both offer identical DX for standard usage. However, when things go wrong:

  • classnames’s exhaustive flattening can mask bugs caused by accidentally passing nested arrays
  • clsx’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.

🔄 Migration and Interoperability

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.

🛠️ When to Prefer Which

Choose clsx when:

  • You’re starting a new project and want the leanest possible dependency
  • Your app has performance-critical rendering paths
  • You follow strict conventions that avoid deeply nested class arrays
  • You use modern tooling (Vite, esbuild) that benefits from its clean ESM export

Choose classnames when:

  • Maintaining a legacy codebase that already uses it
  • Your team values maximum compatibility with unpredictable input shapes
  • You integrate third-party components that assume classnames-style deep flattening
  • You prefer a library with two decades of real-world validation

🔁 Final Recommendation

For 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.

How to Choose: clsx vs classnames
  • clsx:

    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.

  • classnames:

    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.

README for clsx

clsx CI codecov licenses

A tiny (239B) utility for constructing className strings conditionally.
Also serves as a faster & smaller drop-in replacement for the classnames module.

This module is available in three formats:

  • ES Module: dist/clsx.mjs
  • CommonJS: dist/clsx.js
  • UMD: dist/clsx.min.js

Install

$ npm install --save clsx

Usage

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'

API

clsx(...input)

Returns: String

input

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);
//=> ''

Modes

There are multiple "versions" of clsx available, which allows you to bring only the functionality you need!

clsx

Size (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/lite

Size (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 });
//=> ""

Benchmarks

For snapshots of cross-browser results, check out the bench directory~!

Support

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.x and beware of #17.

Tailwind Support

Here some additional (optional) steps to enable classes autocompletion using clsx with Tailwind CSS.

Visual Studio Code
  1. Install the "Tailwind CSS IntelliSense" Visual Studio Code extension

  2. 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);

Related

  • obj-str - A smaller (96B) and similiar utility that only works with Objects.

License

MIT © Luke Edwards