clsx vs classnames vs tailwind-merge
Conditional CSS Class Composition in Modern Frontend Applications
clsxclassnamestailwind-mergeSimilar Packages:

Conditional CSS Class Composition in Modern Frontend Applications

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.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
clsx28,636,7159,7138.55 kB142 years agoMIT
classnames017,83123.6 kB92 years agoMIT
tailwind-merge05,596984 kB21a month agoMIT

Conditional Class Composition: classnames vs clsx vs tailwind-merge

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.

🧱 Core Functionality: What Each Package Actually Does

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.

⚙️ Performance Characteristics

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]
);

🧪 Real-World Usage Scenarios

Scenario 1: Basic Conditional Classes (No Tailwind Conflicts)

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.

Scenario 2: Dynamic Theming or Overrides with Tailwind

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.

Scenario 3: Mixing Framework-Based and Utility Classes

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.

🔁 Composability and Ecosystem Fit

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

🛑 When Not to Use Each

  • Don’t use classnames in new projects unless you have a specific compatibility requirement. clsx is a drop-in replacement with better optimization.
  • Don’t use clsx alone in a Tailwind project where consumers pass arbitrary class overrides — you’ll get unpredictable styling.
  • Don’t use tailwind-merge for non-Tailwind classes. It adds unnecessary overhead and won’t resolve conflicts in other systems.

✅ The Bottom Line

  • For general-purpose class composition: reach for clsx.
  • For Tailwind-heavy apps with dynamic overrides: use tailwind-merge, ideally wrapped with clsx via a cn helper.
  • Reserve 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.

How to Choose: clsx vs classnames vs tailwind-merge

  • clsx:

    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.

  • classnames:

    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.

  • tailwind-merge:

    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.

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