Open-source project
JedWatson/classnames avatar
JedWatson/classnames

classnames: joining conditional CSS classes in JavaScript and React

A simple javascript utility for conditionally joining classNames together

17,773 stars557 forksJavaScriptMIT

At a glance

What is it?
A 2.5.1 utility that turns strings, objects and arrays into a className string. It is small, MIT licensed, and the README is explicit about what it does not do: dedupe by default.
Who is it for?
Adopt classnames if you write React components whose className prop is assembled from several conditions and you want the object form rather than string concatenation. Skip it if your build already has a class-joining helper, or if you need Tailwind conflict resolution, which this package does not perform.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 9 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • JedWatson/classnames17,773 ★ · -4
Star history
Repository2026-09-212026-11-14
JedWatson/classnames17.8k17.8k

-4 Stars in the last 54 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

The problem classnames solves: className strings assembled from conditions

Building a className by hand means appending to a string and guarding every append with an if. The README shows exactly this pattern in its React example, where a button starts as 'btn' and then appends ' btn-pressed' or ' btn-over' depending on state. That code works, but the condition lives in the control flow rather than next to the class name it controls.

classnames moves the condition into the data. The README states that the argument 'foo' is short for { foo: true }, and that if the value associated with a given key is falsy, that key will not appear in the output. So a component can describe its own class list as an object literal and let the utility decide what survives.

The audience is narrow and obvious: anyone rendering CSS class names from JavaScript, most often in React. The package description calls itself a simple utility for conditionally joining classNames together, and the keywords list react, css and classes. It is not a CSS framework, not a styling runtime, and it does not touch the DOM.

How the argument rules work: strings, objects, arrays and falsy values

The function takes any number of arguments, each of which can be a string or an object. A string contributes itself. An object contributes its truthy keys, space separated. The README's example line classNames({ foo: true, bar: true }) returns 'foo bar', and classNames({ 'foo-bar': false }) returns an empty string.

Falsy values are dropped rather than stringified. The README gives classNames(null, false, 'bar', undefined, 0, { baz: null }, '') and the result is 'bar'. That matters in practice because a conditional expression such as false && 'bar' evaluates to false, and false is discarded instead of becoming the literal text "false" in your class attribute.

Arrays are flattened recursively under the same rules. The README's example is const arr = ['b', { c: true, d: false }] passed alongside 'a', which yields 'a b c'. This is what makes it practical to build a list of conditional fragments and pass them all at once.

Because object keys can be computed, the README also documents dynamic class names using ES2015 computed keys, for example classNames({ [`btn-${buttonType}`]: true }). That requires an environment with computed key support, which the README notes is available in ES2015 and Babel.

The repository carries three entry points rather than one. index.js is the default, bind.js is the variant for css-modules that maps names through a bound styles object, and dedupe.js is the alternate version. package.json exposes each of them under the exports map with matching .d.ts files, and the package is marked sideEffects: false, which lets a bundler drop it when unused.

Installing classnames and using it in a React component

The README gives one install command, against the npm registry, using whatever package manager you have:

bash
npm install classnames

The repository's current package metadata declares `type: module` and its source uses an ESM default export. For that revision, use an ESM import and check the actual published package and runtime compatibility before using CommonJS.

js
import classNames from 'classnames';
classNames('foo', 'bar'); // => 'foo bar'

The current source is an ES module; do not assume a classic script tag creates a global without checking the specific distribution version.

For a first real use, the README's React example replaces string concatenation with an object. The component below is adapted from that example; the class names and the conditions are the ones the README uses.

js
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>
  );
}

What you should see is a button whose class attribute is 'btn' plus 'btn-pressed' while pressed, or 'btn btn-over' while hovered and not pressed. The README also shows passing an optional className prop through, which works because only truthy arguments are included.

The dedupe question and where classnames stops

The README is direct about a limitation that surprises people: the main build does not remove duplicate classes. It documents an alternate version, available at the dedupe entry point, which correctly dedupes classes and ensures that falsy classes specified in later arguments are excluded. The repository ships dedupe.js and dedupe.d.ts, so the entry point exists and is typed.

That split is a real decision, not an oversight. Deduping costs work on every call, and the project philosophy section says the package is run millions of times a day in browsers, with updates reviewed for performance implications and a comprehensive test suite. Keeping the fast path free of dedupe logic is consistent with that.

The practical consequence: if two conditional branches can produce the same class, or if a caller passes a class that a later falsy key should remove, the default import will not clean that up. You must import from classnames/dedupe instead. That is a different module, not a flag.

classnames also does not resolve Tailwind conflicts. If 'p-2' and 'p-4' both end up in the output, both are in the string. Nothing in the README claims otherwise.

classnames vs clsx, twMerge and the cn helper

clsx is the closest alternative and the one people compare against most. Both accept strings, objects and arrays and both return a joined string. clsx is a rewrite of the same idea rather than a fork of this code, and the difference that matters for a decision is packaging and maintenance surface, not the call signature. If your codebase already imports clsx, swapping in classnames buys you nothing.

twMerge solves a different problem. It takes a class string and removes conflicting Tailwind utilities, keeping the last one. classnames has no concept of utility conflicts, so a component that layers 'p-2' then 'p-4' gets both classes and lets CSS order decide. That is a bug in Tailwind projects and a non-issue elsewhere.

The cn helper you see in component libraries is not a competitor but a wrapper: it is conventionally classnames or clsx combined with twMerge so that conditional joining and conflict resolution happen in one call. If you need cn behavior, you need two packages, and classnames can be one of them.

There is also bind.js in this repository. package.json exports it as ./bind with bind.d.ts, so it is a supported entry point; the README describes it as the css-modules variant, where classNames.bind(styles) maps names such as 'foo' to the real class values.

Maintenance, versioning and what the MIT licence means here

The last push to the repository was on 2026-10-21, and the repository is not archived. package.json declares version 2.5.1 and the license field is MIT, matching the LICENSE file at the repository root. The README states that classnames follows the SemVer standard for versioning and points at HISTORY.md for the changelog, which is included in the published files list alongside LICENSE and README.md.

MIT is permissive: it allows use, modification and redistribution provided the copyright notice and permission notice are retained. The published package includes the LICENSE file, so the notice travels with the tarball. That is a description of the licence text, not legal advice; if you vendor the source into a product with its own legal review process, run it through that process.

Upgrade cost is low by design. The public surface is a function that returns a string, the types ship as index.d.ts and friends, and the exports map pins the subpaths. The main risk in an upgrade is not API breakage but resolution: because the package sets type to module and exports ./index.js, ./bind and ./dedupe separately, a bundler or test runner with an older resolver may need configuration to reach the subpaths. The scripts in package.json show the project tests with node --test ./tests/*.js and checks types with tsd, so there is a type contract to satisfy if you use TypeScript.

Editorial conclusion

Adopt classnames if you write React components whose className prop is assembled from several conditions and you want the object form rather than string concatenation. Skip it if your build already has a class-joining helper, or if you need Tailwind conflict resolution, which this package does not perform. Before adopting, check which entry point you are importing: index.js does not dedupe, and dedupe.js is the only build that does. Also confirm your bundler resolves the exports map, since package.json sets type to module and lists separate paths for ./bind and ./dedupe.

Frequently asked questions

How do I use classnames in React?

Import it and pass the conditions as an object to the className prop. The README's React example builds a button class with classNames({ btn: true, 'btn-pressed': isPressed, 'btn-over': !isPressed && isHovered }).

How do I add multiple classnames in React?

Pass any number of arguments; strings contribute themselves and object keys contribute when their value is truthy. The README shows classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }) returning 'foo bar baz quux'.

How do I install classnames in React?

The README gives npm install classnames against the npm registry, then a require or import of the package. There is no separate React-specific package; the same module is used in both cases.

What is the difference between classnames and clsx?

Both accept strings, objects and arrays and return a joined class string, so the call signatures overlap. The choice comes down to which package your project already depends on rather than to a documented behavioural difference.

Does classnames resolve conflicting Tailwind classes?

No. classnames joins the classes it is given and has no concept of utility conflicts, so overlapping Tailwind utilities both appear in the output. Resolving those conflicts requires a separate tool such as twMerge.

What are classNames in React?

They are the CSS class names applied to an element through the className prop, which React uses instead of the HTML class attribute. classnames is the utility the README describes for joining those names conditionally.

Official sources

  1. Issues
  2. JedWatson/classnames on GitHub
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/jedwatson-classnames.svg)](https://hysenlabs.com/projects/jedwatson-classnames)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/jedwatson-classnames.svg)](https://hysenlabs.com/projects/jedwatson-classnames)

Was this useful?