tailwind-merge review
tailwind-merge 3.6.0 accepts class-list values and removes earlier Tailwind utilities when a later one would set the same style. It knows that `p-4` conflicts with `p-2`, that responsive or state modifiers form separate scopes, and that `px-3 pr-4` must keep the left padding from `px-3`. It preserves class names it does not recognize. Version 3.6 covers Tailwind CSS through 4.3, adds full-name lookup for slash-bearing classes such as named containers, and accepts readonly arrays. Our CommonJS and ESM checks both worked, and the package includes TypeScript declarations.
tailwind-merge 3.6.0 installed in 0.4 seconds as one 2 MB package, had zero audit findings, and added 8.7 KB gzipped in our browser bundle. Pay that client cost when callers are meant to override Tailwind utilities; a finite variant API is safer when the component owner should control every supported style.
We installed it
| Install | ✓ · 0.4s | 1 package on disk · 2 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package with exports map |
| Browser | 8.7 KB | gzipped (28.2 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 tailwind-merge install cleanly?
Yes. In a fresh container with an empty cache, npm install tailwind-merge finished in 0.4s, leaving 1 package and 2 MB on disk. npm audit reported no known vulnerabilities.
How much does tailwind-merge add to a browser bundle?
8.7 KB gzipped (28.2 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does tailwind-merge work with both ESM and CommonJS?
Yes. Both import 'tailwind-merge' and require('tailwind-merge') worked in Node 22 in our run. The package is published as CommonJS with an exports map.
Does tailwind-merge include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
tailwind-merge or clsx: which should you use?
clsx: Choose it for conditional class joining when the code does not need Tailwind-aware conflict removal. tailwind-merge 3.6.0 installed in 0.4 seconds as one 2 MB package, had zero audit findings, and added 8.7 KB gzipped in our browser bundle.
When should you not use tailwind-merge?
The project still uses Tailwind CSS 3. The 3.6.0 README points that generation to tailwind-merge 2.6 rather than the current major.
Discussed on
Use it if
- A component combines fixed Tailwind utilities with a caller's `className`, and the caller is intentionally allowed to override layout or color.
- Class strings pass through several component layers where later padding, display, state, or responsive utilities should win predictably.
- A Tailwind 4 design system has a small set of custom utilities that can be described in a maintained merge configuration.
- Conditional strings and nested readonly arrays should be joined and then resolved by Tailwind class meaning.
- The project still uses Tailwind CSS 3. The 3.6.0 README points that generation to tailwind-merge 2.6 rather than the current major.
- A closed variant API already expresses every supported component state. Accepting arbitrary override classes can make internal style refactors break consumers.
- You expect it to inspect custom CSS or `@apply`. Unknown classes remain in the output even when their declarations conflict with recognized utilities.
- Your code mixes standard utilities with equivalent arbitrary properties or modifiers. The documented limits keep `p-4 [padding:1rem]` and `[&:focus]:ring focus:ring-4` together.
- The feature only needs conditional concatenation and 8.7 KB gzipped matters. `twJoin` or clsx performs joining without shipping the class-conflict model we measured.
Setup reality
We installed tailwind-merge 3.6.0 in a clean Node 22 Bookworm container. npm finished in 0.4 seconds and left one package occupying 2 MB. The package has zero direct dependencies and zero peers, with 1080 KB unpacked. npm audit found zero vulnerabilities at every severity. TypeScript declarations are included. The CommonJS package has an exports map, and both require() and ESM import succeeded in Node 22.23.2.
Our esbuild browser test imported the full package and produced 28.2 KB minified and 8.7 KB gzipped. No plugin, stylesheet scan, credential, or config file is needed for standard Tailwind 4 classes because the conflict table ships in the runtime bundle. Put the caller's classes last in twMerge(defaults, className). Use twJoin when conditions already guarantee that the internal class choices do not conflict; it skips semantic resolution.
The parser reads utility syntax rather than CSS declarations. It retains unknown names and does not compare an arbitrary property with its standard equivalent. Ambiguous arbitrary values may need labels such as length:, color:, size:, or family-name:. Custom theme values often fit existing validators, but new utility meanings, prefixes, class groups, and order-sensitive modifiers need extendTailwindMerge. Define the extended merger once at module scope because configuration builds a large lookup structure on first use.
Later conflicting classes win, while asymmetric cases preserve unaffected sides. Results are cached after arguments are joined, with a documented default capacity of 500 strings. Patch releases may change output when a conflict bug is corrected, and minor releases may start recognizing newer Tailwind syntax. Version 3.6.0 adds Tailwind 4.3 and named-container slash lookup, so snapshot the final classes of sensitive components when upgrading even if the JavaScript API does not change.
Patterns
Let a caller replace component defaults merge-caller-override
import { twMerge } from 'tailwind-merge';
function Button({ className, ...props }) {
return (
<button
{...props}
className={twMerge('rounded-md bg-blue-600 px-3 py-2 text-white', className)}
/>
);
}Pass `className` last. A caller value such as `bg-red-600 px-5` replaces only the conflicting background and horizontal padding.
Join known internal choices join-conditional-classes
import { twJoin } from 'tailwind-merge';
const className = twJoin(
'grid gap-2',
disabled && 'cursor-not-allowed opacity-50',
active ? 'border-blue-500' : 'border-slate-300',
);`twJoin` handles strings, arrays, and falsy values without conflict analysis. It does not accept the object-map form supported by clsx.
Merge responsive and state utilities resolve-modifier-scope
twMerge(
'p-2 hover:bg-slate-100 md:p-4',
'hover:bg-slate-200 md:p-6',
);
// 'p-2 hover:bg-slate-200 md:p-6'Base, hover, and `md` utilities are separate scopes. A class removes an earlier conflict only when the modifier scope matches.
Keep unaffected padding sides preserve-padding-refinement
twMerge('p-3 px-5'); // 'p-3 px-5'
twMerge('pr-4 px-3'); // 'px-3'
twMerge('px-3 pr-4'); // 'px-3 pr-4'Conflict direction is asymmetric: a later axis utility replaces an earlier side, while a later side leaves the other axis side intact.
Disambiguate a font family variable label-arbitrary-font-value
twMerge(
'font-(family-name:--brand)',
'font-(--weight)',
);
// 'font-(family-name:--brand) font-(--weight)'Without `family-name:`, both arbitrary `font-*` variables default to font-weight and the first value can be removed incorrectly.
Mark an arbitrary value as a length label-arbitrary-text-size
twMerge(
'text-[length:theme(myScale.heading)]',
'text-lg',
);
// 'text-lg'The `length:` label identifies a font-size value. An unlabeled `theme(...)` expression may be treated as a color and survive beside `text-lg`.
Resolve important classes merge-important-utilities
twMerge('p-3! p-4! p-5');
// 'p-4! p-5'
twMerge('right-2! -inset-x-1!');
// '-inset-x-1!'Important utilities conflict with important members of their groups. A non-important utility remains a separate class in the output.
Resolve a postfix modifier merge-line-height-shorthand
twMerge('text-sm leading-6 text-lg/7');
// 'text-lg/7'The `/7` postfix supplies line height with the new font size, so the earlier `text-sm` and `leading-6` are both removed.
Preserve unrecognized class names retain-custom-classes
twMerge('card-shell p-5 p-2 analytics-hook');
// 'card-shell p-2 analytics-hook'Unknown classes remain, but tailwind-merge cannot tell whether `card-shell` also sets padding and conflicts with `p-2`.
Register custom theme tokens extend-theme-values
import { extendTailwindMerge } from 'tailwind-merge';
const appMerge = extendTailwindMerge({
extend: {
theme: {
text: ['display'],
spacing: ['gutter'],
},
},
});
appMerge('text-sm text-display p-2 p-gutter');Create `appMerge` once at module scope. These keys follow tailwind-merge's runtime config, not a complete Tailwind configuration object.
Describe a custom utility family add-custom-class-group
const appMerge = extendTailwindMerge({
extend: {
classGroups: {
elevation: [{ elevation: ['low', 'medium', 'high'] }],
},
},
});
appMerge('elevation-low elevation-high');
// 'elevation-high'Members of one class group replace earlier members. The `elevation` ID is internal to the merge configuration.
Make a reset remove two custom groups configure-directed-conflict
const appMerge = extendTailwindMerge({
extend: {
classGroups: {
'aspect-w': [{ 'aspect-w': ['1', '2', '3'] }],
'aspect-h': [{ 'aspect-h': ['1', '2', '3'] }],
'aspect-reset': ['aspect-none'],
},
conflictingClassGroups: {
'aspect-reset': ['aspect-w', 'aspect-h'],
},
},
});The rule is directional. A later `aspect-none` removes preceding width and height utilities; the reverse needs a separate conflict rule if required.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| clsx | npm | Choose it for conditional class joining when the code does not need Tailwind-aware conflict removal. |
| class-variance-authority | npm | Choose it when a typed, finite variant contract should control component styling instead of open-ended overrides. |
| tailwind-variants | npm | Choose it when variants, compound variants, slots, and Tailwind conflict handling belong in one component definition. |
| classnames | npm | Choose it where its string, array, and object-map API is already the established project convention. |
More web frontend guides
postcss · react · react-dom · tailwindcss · htmlparser2 · @tanstack/react-query · 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.

