clsx review
A class named 'false' still appears if you pass it as a string. clsx 2.1.1 joins conditional class names into one string, using JavaScript truthiness to decide which inputs to include. Strings supply names, objects supply names through their keys, and arrays group inputs that follow the same rules. It suits the point where a component's state becomes its class attribute. It neither generates CSS nor removes duplicate or conflicting classes. The useful decision is whether those simple inclusion rules match your component's contract.
Use clsx when a component needs conditional class joining and you can keep the input rules explicit. Before replacing another helper, check zeroes, string booleans, and any expectation that a later argument removes an earlier class.
Answers from our run
clsx or classnames: which should you use?
classnames: Choose classnames 2.5.1 when you need its optional dedupe entry, which can remove a class through a later false object value, or its CSS Module bind entry. For that decision, see the existing classnames guide. Use clsx when a component needs conditional class joining and you can keep the input rules explicit.
When should you not use clsx?
You only have a fixed class string or one short condition. A dependency may add little clarity.
Use it if
- Your component has several conditional classes and a template literal has become hard to read. Object keys make each class and its inclusion condition visible together.
- You receive optional class strings and arrays from callers and want one place to flatten and join them.
- You want to assemble class attributes in plain JavaScript without a framework-specific styling API.
- You can keep styling rules in CSS and only need to select existing class names at runtime.
- You only have a fixed class string or one short condition. A dependency may add little clarity.
- A caller expects a later false flag to remove a previously included class. clsx appends each matching input and does not undo earlier ones.
- Your API promises that later Tailwind utilities replace earlier conflicting utilities. clsx keeps both, so that promise needs a separate conflict resolver.
- Your data uses strings such as 'false' to represent booleans. Normalize that data first or conditional classes will appear unexpectedly.
- You need class-name deduplication or automatic CSS Module name binding. The alternative entry points in classnames address those jobs more directly.
Setup reality
Install the version discussed here with npm install clsx@2.1.1, then use import clsx from 'clsx' or import { clsx } from 'clsx'. The version-specific npm response lists separate import and default export targets and bundled TypeScript declarations. These are package declarations, not a compatibility test of your application. This guide inspected primary documentation and source code. We did not install the package, execute the examples, run an audit, or measure a browser bundle. Every output comment below is a source-derived illustration.
Most of the work is deciding what your inputs mean. Zero, an empty string, null, undefined, false, and NaN are discarded. A nonzero number becomes text. A standalone true contributes nothing. In an object, the key is the class name and the value is only its inclusion test: { active: 'false' } includes active. Convert form or API strings to booleans deliberately instead of trusting their spelling. A zero-valued identifier needs explicit string conversion if it is meant to be a literal class name.
Objects and arrays also create migration traps. A later { active: false } does not cancel an earlier 'active', and repeated strings stay repeated. Use plain class maps: the object branch uses for...in without an own-property check, so enumerable inherited keys can also contribute names. Pass actual CSS Module values when you have them rather than expecting clsx to resolve local names.
The lite entry accepts only strings and ignores other inputs. Switching imports while leaving object or array arguments in place silently loses those classes. There is a separate type/runtime wrinkle: the 2.1.1 release added bigint to ClassValue, but the implementation only converts strings and numbers to text. Convert a bigint explicitly if its text is intentional.
Product scores below are editorial judgments from these documents, on a 1-5 scale: 1 means poor fit or major documented gaps, 3 means adequate evidence with material limits, and 5 means unusually complete evidence for this narrow job. API assesses the exposed contract, docs assess explanation of inputs and failure cases, maintenance assesses release evidence, and ecosystem assesses integration surfaces. Unknown adoption, future support, and untested runtime compatibility earn no extra credit.
Patterns
Keep a base class and add a state join-conditional-strings
import clsx from 'clsx';
const selected = true;
const className = clsx('tab', selected && 'tab-selected');
// Expected: 'tab tab-selected'Source-derived, unexecuted illustration for 2.1.1. A false condition contributes no class. A standalone boolean true also contributes nothing.
Make the condition visible beside the class use-class-map
import { clsx } from 'clsx';
const className = clsx('field', {
'field-invalid': true,
'field-disabled': false,
});
// Expected: 'field field-invalid'The object key supplies the class text. Its value only decides inclusion; the value is not appended to the result.
Combine nested groups flatten-class-groups
import clsx from 'clsx';
const callerClasses = ['card-wide', [null, 'card-raised']];
const className = clsx('card', callerClasses);
// Expected: 'card card-wide card-raised'The main entry recursively processes arrays in order. This behavior is absent from the string-only lite entry.
Parse a string flag before joining normalize-string-booleans
import clsx from 'clsx';
const rawFlag = 'false';
clsx({ active: rawFlag });
// Expected: 'active'
clsx({ active: rawFlag === 'true' });
// Expected: ''A nonempty string is truthy even when it spells false. The explicit comparison here assumes your input format uses exactly 'true' to mean enabled.
Convert a numeric class deliberately preserve-zero-text
import clsx from 'clsx';
clsx('item', 0, 2);
// Expected: 'item 2'
clsx('item', String(0));
// Expected: 'item 0'Numeric zero is discarded before conversion, while a nonzero number becomes text. Explicit conversion keeps the literal string '0' if your CSS really uses it.
Do not treat false as a removal instruction understand-append-only
import clsx from 'clsx';
clsx('active', { active: false }, 'active');
// Expected: 'active active'
clsx('p-2', 'p-4');
// Expected: 'p-2 p-4'The implementation appends accepted values and never removes earlier tokens. It does not deduplicate names or resolve CSS conflicts. Do not interpret the string's last token as a guarantee about computed styles.
Join resolved CSS Module values pass-module-values
import clsx from 'clsx';
// Stand-in for values supplied by your CSS Modules build.
const styles = { root: 'root_a1', active: 'active_b2' };
const className = clsx(styles.root, { [styles.active]: true });
// Expected: 'root_a1 active_b2'This illustrates ordinary string and computed-key inputs. clsx does not bind local names to a CSS Module map; your build supplies the resolved values.
Use a plain class map avoid-inherited-classes
import clsx from 'clsx';
const flags = Object.create({ inherited: true });
flags.local = true;
clsx(flags);
// Expected: 'local inherited'
clsx({ local: flags.local });
// Expected: 'local'The 2.1.1 object branch uses for...in and no own-property check. Building an explicit map avoids including enumerable prototype keys.
Keep lite calls string-only use-string-only-lite
import clsx from 'clsx/lite';
clsx('chip', true && 'chip-active');
// Expected: 'chip chip-active'
clsx('chip', { 'chip-active': true }, ['chip-wide']);
// Expected: 'chip'The pinned README says the lite entry ignores every non-string argument. An import-only migration can therefore drop existing object and array classes without an exception.
Separate accepted types from runtime text convert-bigint-explicitly
import clsx from 'clsx';
const id = 7n;
clsx('record', id);
// Expected: 'record'
clsx('record', String(id));
// Expected: 'record 7'The 2.1.1 release adds bigint to ClassValue, but its runtime converts only strings and numbers. This illustration requires JavaScript syntax support for bigint, which we have not tested in your build.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| classnames | npm | Choose classnames 2.5.1 when you need its optional dedupe entry, which can remove a class through a later false object value, or its CSS Module bind entry. For that decision, see the existing [classnames guide](https://mrkeyoor.com/libs/classnames/). |
| tailwind-merge | npm | Choose tailwind-merge when Tailwind utility conflicts are the problem. Its 3.6.0 documentation targets Tailwind 4.0 through 4.3 and directs Tailwind 3 users to 2.6.0. Check your CSS version before choosing a release; the [tailwind-merge guide](https://mrkeyoor.com/libs/tailwind-merge/) covers that separate decision. |
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.