mrkeyoor.com_
Sun 20 Sept 02:44 UTC
npmWeb Frontendupdated 18 Sept 2026

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.

75.5Mdownloads / wk
Verdict

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

Lab card: what happened when we installed tailwind-mergeScreenshot of tailwind-merge documentation
Install✓ · 0.4s1 package on disk · 2 MB
ImportESM import works · require() works · CommonJS package with exports map
Browser8.7 KBgzipped (28.2 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 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.

API stability4/5The public surface remains centered on `twMerge`, `twJoin`, `extendTailwindMerge`, and `createTailwindMerge`, with bundled declarations that support both module loaders in our check. The project follows semantic versioning for JavaScript API breaks. Its versioning guide is candid that a patch can alter applied styles after a conflict fix, while a minor can recognize newer Tailwind classes and therefore change the returned string.
Docs5/5The GitHub documentation returned HTTP 200 and separately explains the component-override use case, reasons to avoid merging, conflict rules, arbitrary-value labels, arbitrary property and variant limits, caching, configuration, recipes, plugins, API types, and versioning. Examples cover asymmetric padding and order-sensitive modifiers. The docs also state which Tailwind generations each package major supports, preventing a common version mismatch.
Maintenance5/5GitHub showed 5,678 stars, an unarchived repository, a push on 2026-08-26, and 26 open issues and pull requests. Release 3.6.0 shipped in May with Tailwind CSS 4.3 support, readonly array values, and `postfixLookupClassGroups` for slash-bearing named containers. The preceding releases tracked Tailwind 4.1 and 4.2 and corrected arbitrary color and font classification, showing regular alignment with upstream syntax.
Ecosystem5/5npm counted 83,804,644 downloads in the latest completed week. The package has zero dependencies and peers, includes TypeScript declarations, accepts nested conditional lists, and loaded through CommonJS and ESM in our sandbox. Its inputs resemble clsx-style composition, while extension hooks serve custom Tailwind systems. The boundary is strict: version 3 models Tailwind 4, and Tailwind 3 projects belong on the older 2.6 line.

Discussed on

  1. hnShow HN: tailwind-merge – Merge Tailwind CSS classes without style conflicts3 points
  2. hnMerge Tailwind CSS classes without style conflicts3 points

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

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

PackageRegistryPick it when
clsxnpmChoose it for conditional class joining when the code does not need Tailwind-aware conflict removal.
class-variance-authoritynpmChoose it when a typed, finite variant contract should control component styling instead of open-ended overrides.
tailwind-variantsnpmChoose it when variants, compound variants, slots, and Tailwind conflict handling belong in one component definition.
classnamesnpmChoose 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.