classnames review
classnames 2.5.1 is a single function that turns strings, nested arrays, and objects such as `{ active: isActive }` into a space-separated class attribute. False values, null, undefined, empty strings, and zero are omitted. Two optional entry points change that narrow job: `classnames/bind` maps local CSS Module names through a styles object, while `classnames/dedupe` removes repeated tokens and lets a later false object value cancel an earlier class. Version 2.5.1 only removed an accidentally published `workspaces` field. The preceding 2.5.0 release added the exports map and repaired a TypeScript input regression.
classnames 2.5.1 installed in 0.8 seconds and added 1 package, 1 MB on disk, and 0 audit findings in our sandbox. Keep it for its settled input rules or its bind and dedupe entries; new Tailwind code still needs a conflict-aware tool.
We installed it
| Install | ✓ · 0.8s | 1 package on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package with exports map |
| Browser | 0.8 KB | gzipped (1.4 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does classnames install cleanly?
Yes. In a fresh container with an empty cache, npm install classnames finished in 0.8s, leaving 1 package and 1 MB on disk. npm audit reported no known vulnerabilities.
How much does classnames add to a browser bundle?
0.8 KB gzipped (1.4 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does classnames work with both ESM and CommonJS?
Yes. Both import 'classnames' and require('classnames') worked in Node 22 in our run. The package is published as CommonJS with an exports map.
Does classnames include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
classnames or clsx: which should you use?
clsx: Choose it for familiar conditional-class calls in new code that does not need bind or dedupe subpaths. classnames 2.5.1 installed in 0.8 seconds and added 1 package, 1 MB on disk, and 0 audit findings in our sandbox.
When should you not use classnames?
Two fixed strings are all you ever join. A template literal is clearer than adding a package for that case.
Use it if
- A component mixes a permanent class with boolean state classes and an optional className supplied by its caller.
- CSS Modules are in use and `classnames/bind` can translate readable local names through the imported styles map.
- One shared helper must accept strings, objects, and nested arrays across both CommonJS and ESM consumers.
- Existing code already depends on the `dedupe` entry's ability to cancel a class with a later false value.
- Two fixed strings are all you ever join. A template literal is clearer than adding a package for that case.
- Conflicting Tailwind utilities must be resolved. The default function can emit both `p-2` and `p-4`; it has no knowledge of Tailwind's conflict groups, while `tailwind-merge` does.
- A native ESM implementation or named runtime exports are required. Version 2.5.1 is CommonJS and exposes the function through default-import interop.
- Later false values must remove earlier classes on a hot render path. Only the dedupe entry has that behavior, and the README says it runs about five times slower.
- Your team wants a smaller API for new code and has no need for CSS Modules binding or deduplication. `clsx` covers the common string, array, and object calls.
Setup reality
We installed classnames 2.5.1 in a fresh Node 22 Bookworm sandbox in 0.8 seconds. The run left 1 package and 1 MB on disk. npm audit reported 0 known vulnerabilities. The published package has 0 direct dependencies, 0 peer dependencies, occupies 56 KB unpacked, and includes its TypeScript declarations. Both require() and ESM import worked even though the implementation is CommonJS behind an exports map.
Setup ends at the import. There are no credentials, native extensions, configuration files, or install hooks. The exports map lists the main function plus classnames/bind and classnames/dedupe. Release 2.5.1 changes package metadata by removing the stray workspaces field; argument handling is the same as 2.5.0.
The default entry joins what you give it and keeps duplicates. If p-2 and p-4 both survive, CSS ordering decides the result; classnames does not choose the later Tailwind utility. The dedupe build can cancel an earlier token when a later object maps that token to false, at the runtime cost documented in the README.
Our browser build measured 1.4 KB minified and 0.8 KB gzipped. Nested arrays are flattened recursively, while numeric zero disappears as a false value. Runtime strings assembled from color fragments also stay invisible to static Tailwind content scanning. Put complete utility strings in a lookup object when the CSS build needs to discover them.
Patterns
Join fixed class tokens join-fixed-classes
import classNames from 'classnames';
const classes = classNames('card', 'card--raised');
// 'card card--raised'Version 2.5.1 inserts spaces between truthy tokens; it does not check whether either selector exists in CSS.
Select classes from component state toggle-state-classes
const classes = classNames('button', {
'button--pressed': pressed,
'button--saving': saving,
'button--disabled': !canSubmit,
});Every truthy object property is included, so overlapping booleans can emit multiple state selectors.
Combine a caller className accept-classname-prop
function Panel({ className, open, children }) {
return (
<section className={classNames('panel', { open }, className)}>
{children}
</section>
);
}An undefined className prop is discarded, so an empty-string default adds nothing.
Flatten grouped class inputs flatten-nested-arrays
const base = ['layout', ['panel', { hidden: !open }]];
const classes = classNames(base, extra);Arrays are processed recursively. A numeric 0 is false and vanishes unless you convert it to the string '0'.
Choose a modifier class select-dynamic-name
const modifiers = {
info: 'alert--info',
warning: 'alert--warning',
};
const classes = classNames('alert', modifiers[tone]);A lookup table keeps the complete selector visible in source and avoids producing undefined for a known tone.
Expose Tailwind utilities to the scanner preserve-tailwind-literals
const tones = {
info: 'bg-blue-100 text-blue-900',
danger: 'bg-red-100 text-red-900',
};
const classes = classNames('rounded px-3 py-2', tones[tone]);Complete strings can be found by Tailwind's content scan; fragments assembled during execution usually cannot.
Translate CSS Module names bind-css-module
import bindClassNames from 'classnames/bind';
import styles from './button.module.css';
const cx = bindClassNames.bind(styles);
const classes = cx('root', { active });The bind entry looks up each token in the styles object. Unmapped names pass through, so a typo may remain unnoticed.
Use the dedupe entry remove-duplicates
import dedupeClassNames from 'classnames/dedupe';
const classes = dedupeClassNames('row', 'row', { selected: true });
// 'row selected'The README measures the dedupe variant at about five times slower than the default function.
Turn off an earlier class cancel-previous-token
import dedupeClassNames from 'classnames/dedupe';
const classes = dedupeClassNames('enabled', {
enabled: false,
disabled: true,
});
// 'disabled'Later false mappings cancel earlier tokens only in the dedupe entry; the main entry would retain enabled.
Load the CommonJS function load-with-require
const classNames = require('classnames');
module.exports = classNames('widget', { ready: true });Our Node 22 sandbox loaded version 2.5.1 with require() successfully.
Default-import from ESM load-with-esm
import classNames from 'classnames';
const classes = classNames('widget', { ready });ESM import worked in our sandbox through CommonJS default interop; the package has no named runtime functions.
Resolve Tailwind conflicts after joining merge-tailwind-conflicts
import classNames from 'classnames';
import { twMerge } from 'tailwind-merge';
const classes = twMerge(classNames('p-2 text-sm', wide && 'p-4'));classnames can return both padding tokens. tailwind-merge applies Tailwind-specific conflict rules and retains p-4 when wide is true.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| clsx | npm | Choose it for familiar conditional-class calls in new code that does not need bind or dedupe subpaths. |
| tailwind-merge | npm | Choose it when later Tailwind utilities must replace conflicting earlier utilities. |
| classcat | npm | Choose it for a small array-and-object class combiner when classnames compatibility is irrelevant. |
More web frontend guides
postcss · react · react-dom · tailwindcss · htmlparser2 · tailwind-merge · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

