mrkeyoor.com_
Sun 20 Sept 07:00 UTC
npmWeb Frontendupdated 20 Sept 2026

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.

Verdict

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

Lab card: what happened when we installed classnamesScreenshot of classnames documentation
Install✓ · 0.8s1 package on disk · 1 MB
ImportESM import works · require() works · CommonJS package with exports map
Browser0.8 KBgzipped (1.4 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 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.

API stability5/5The 2.x call contract still accepts strings, nested arrays, objects, custom string values, and false inputs with the same output rules documented for years. HISTORY.md records packaging, typing, and speed fixes rather than recurring rewrites of the function. Release 2.5.1 removes one package metadata field, and the exports map keeps the established bind and dedupe paths addressable.
Docs4/5The README shows exact outputs for strings, object conditions, false values, nested arrays, computed names, React props, CSS Modules binding, and deduplication. It also states the dedupe performance cost and links a release-by-release history. Modern gaps remain: the CommonJS interop story is implicit, and readers get no warning that class joining cannot settle Tailwind conflicts or expose generated fragments to a class scanner.
Maintenance4/5GitHub reported an unarchived repository pushed on August 24, 2026, with 17,783 stars and 11 open issues and pull requests. npm still points to 2.5.1, published in December 2023. A long release interval is unsurprising for this small, stable function, though requests for native ESM or new behavior should be judged against the current package rather than assumed to be coming soon.
Ecosystem5/5npm counted 34,179,202 downloads during the latest completed week, and the argument style appears throughout React component examples and packages that emulate its API. Our install confirmed bundled declarations, working CommonJS loading, working ESM import interop, and explicit exports for bind and dedupe. That coverage makes it easy to retain across old Node code and current frontend builds.

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.
Skip it if

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

PackageRegistryPick it when
clsxnpmChoose it for familiar conditional-class calls in new code that does not need bind or dedupe subpaths.
tailwind-mergenpmChoose it when later Tailwind utilities must replace conflicting earlier utilities.
classcatnpmChoose 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.