mrkeyoor.com_
Thu 06 Aug 01:03 UTC
npmWeb Frontendupdated 05 Aug 2026

tailwind-merge

tailwind-merge resolves Tailwind CSS class conflicts in JavaScript at runtime. Call twMerge('px-2 py-1', 'p-3') and it returns 'p-3', because it knows px, py, and p fight over the same CSS properties and the last one you passed should win. That is the whole job: it understands Tailwind's class groups, modifiers like hover: and md:, arbitrary values like bg-[#B91C1C], and the important prefix, and it drops earlier classes that a later class overrides. This matters for reusable components that accept a className prop, where plain string concatenation leaves both classes in the DOM and CSS specificity, not your prop order, decides which style applies. Version 3.x supports Tailwind CSS v4.0 through v4.3.

Verdict

The de facto standard for one specific, real problem: letting a passed-in className reliably override component defaults in Tailwind projects, proven at about 79M weekly downloads. Use it at component boundaries and skip it everywhere the classes are fully under your control.

API stability4/5twMerge, twJoin, and extendTailwindMerge have kept their shape across majors; version bumps are driven by Tailwind itself, so a Tailwind 3 to 4 migration forces a tailwind-merge major with config changes at the same time.
Docs5/5The docs folder covers what it is for, when not to use it, limitations, configuration, recipes, and a full API reference, all version-linked; the limitations page being honest about drift with custom themes is rare and welcome.
Maintenance4/5Pushed August 2026 with releases tracking each Tailwind 4.x minor and a small tracker (about 30 open issues and PRs), but it is essentially one maintainer (dcastil) carrying a dependency that half the React ecosystem sits on.
Ecosystem5/5About 79M weekly downloads, the cn helper in shadcn/ui is built on it, tailwind-variants uses it internally, and there is a documented plugin API for third-party class groups.

Use it if

  • You build components with a className override prop and need the caller's p-4 to actually beat your internal p-2 instead of losing a specificity coin flip
  • You use shadcn/ui or code in that style, where the cn helper (clsx piped into twMerge) is assumed by every copied component
  • Your class strings mix conditionals, modifiers, and arbitrary values, and hand-deduplicating conflicts has already produced visual bugs
  • You maintain a design system on Tailwind v4 and want conflict resolution that understands the default theme, including custom --color-* values, with zero config
Skip it if

Setup reality

npm install tailwind-merge, zero dependencies, TypeScript types included, and twMerge works immediately with the full default Tailwind v4 theme; custom colors even work unconfigured because the color validator is permissive. The friction arrives with any other theme customization: a custom --text-huge or --spacing-gutter merges wrongly until you mirror it in extendTailwindMerge, and nothing warns you, styles are just occasionally wrong. Version coupling is the other trap: tailwind-merge majors track Tailwind majors, so Tailwind 3 projects stay on 2.6.0 while v3.x tracks Tailwind 4.0 to 4.3. Also call extendTailwindMerge once at module top level; it builds a large structure and is too expensive to create inside a render function.

Patterns

Merge classes so the last one winsmerge-conflicting-classes

import { twMerge } from 'tailwind-merge'

twMerge('px-2 py-1 bg-red-500', 'p-3 bg-blue-600')
// -> 'p-3 bg-blue-600'

twMerge('hover:bg-red-500', 'hover:bg-blue-600')
// -> 'hover:bg-blue-600'

Only conflicting classes are removed; everything else passes through in order. Modifiers are compared too, so hover:bg-* never clobbers plain bg-*.

The standard cn helper with clsxcn-helper

import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

cn('p-2', isActive && 'bg-blue-500', { 'p-4': isLarge })
// object and boolean syntax from clsx, conflict resolution from twMerge

twMerge itself does not accept object syntax, only strings, arrays, and falsy values; clsx handles the conditional shapes first. This is the exact helper shadcn/ui ships.

Let a className prop override component defaultscomponent-classname-override

import { twMerge } from 'tailwind-merge'

function Button({ className, ...props }) {
  return (
    <button
      className={twMerge('rounded bg-gray-200 px-4 py-2', className)}
      {...props}
    />
  )
}

// <Button className="bg-blue-600 px-6" /> renders
// 'rounded py-2 bg-blue-600 px-6'

This is the core use case. With plain template strings both bg- classes stay in class attribute and the stylesheet order decides, which is why overrides randomly fail without twMerge.

Pass conditionals straight to twMergeconditional-classes

import { twMerge } from 'tailwind-merge'

twMerge(
  'flex items-center',
  isDisabled && 'opacity-50 pointer-events-none',
  size === 'lg' ? 'h-12 px-6' : 'h-9 px-4',
)

false, null, undefined, and empty strings are ignored, so short-circuit expressions work directly. For { 'class': condition } object style you still need clsx in front.

Use twJoin when nothing can conflictjoin-without-merging

import { twJoin } from 'tailwind-merge'

twJoin(
  'border border-red-500',
  hasBackground && 'bg-red-100',
  hasLargeText && 'text-lg',
)

twJoin only concatenates and skips falsy values, with none of the conflict-resolution cost. Prefer it inside components where you wrote every class and conflicts are impossible.

Teach it your custom theme valuescustom-theme-scale

import { extendTailwindMerge } from 'tailwind-merge'

// CSS: @theme { --text-huge: 100px; --spacing-gutter: 1.5rem; }

export const twMerge = extendTailwindMerge({
  extend: {
    theme: {
      text: ['huge'],
      spacing: ['gutter'],
    },
  },
})

twMerge('text-lg text-huge') // -> 'text-huge'
twMerge('p-4 p-gutter')      // -> 'p-gutter'

tailwind-merge cannot read your CSS, so custom theme scales merge wrongly until mirrored here. Exception: custom --color-* values work without config because the color validator accepts any name.

Register your own utility classescustom-class-groups

import { extendTailwindMerge } from 'tailwind-merge'

const twMerge = extendTailwindMerge<'badge'>({
  extend: {
    classGroups: {
      badge: ['badge', 'badge-pill', { 'badge-dot': ['', 'sm', 'lg'] }],
    },
  },
})

twMerge('badge badge-pill') // -> 'badge-pill'

New class group IDs must be passed as a TypeScript generic or the strictly typed config rejects them. Define the merged function once at top level; building it is expensive.

Handle a Tailwind prefixprefix-support

import { extendTailwindMerge } from 'tailwind-merge'

// CSS: @import 'tailwindcss' prefix(tw);

const twMerge = extendTailwindMerge({
  prefix: 'tw',
})

twMerge('tw:px-2 tw:py-1', 'tw:p-3') // -> 'tw:p-3'

In Tailwind v4 the prefix appears as a variant (tw:flex), and the config takes it without the dash. On tailwind-merge 2.x for Tailwind 3 the same option was written 'tw-'.

Merge arbitrary values and propertiesarbitrary-values

import { twMerge } from 'tailwind-merge'

twMerge('bg-red-500', 'bg-[#B91C1C]')
// -> 'bg-[#B91C1C]'

twMerge('p-4', 'p-[3.5rem]')
// -> 'p-[3.5rem]'

twMerge('[mask-type:luminance]', '[mask-type:alpha]')
// -> '[mask-type:alpha]'

Arbitrary values are parsed into the same class groups as their named cousins. Arbitrary variants work too, but the docs note some order-sensitive edge cases where merging stays conservative.

Pick the right version for Tailwind 3tailwind-v3-projects

# Tailwind CSS v4 project
npm install tailwind-merge

# Tailwind CSS v3 project: stay on the 2.x line
npm install tailwind-merge@2.6.0

tailwind-merge 3.x only understands Tailwind v4 class names and theme keys; on a Tailwind 3 codebase it will mis-merge. The 2.6.0 docs live under the v2.6.0 tag on GitHub.

Skip twMerge where classes are staticwhen-not-to-merge

// No outside classes can arrive here, so no merge needed:
function Badge({ children }) {
  return (
    <span className="inline-flex rounded-full bg-gray-100 px-2 text-xs">
      {children}
    </span>
  )
}

// Only the boundary that accepts className pays for twMerge:
function Card({ className, ...props }) {
  return <div className={twMerge('rounded-lg border p-4', className)} {...props} />
}

Every twMerge call parses its inputs at runtime. The library's own docs recommend using it at component boundaries, not as a blanket replacement for writing class strings.

Alternatives

PackageRegistryPick it when
clsxnpmYou need conditional class composition only, without conflict resolution; it is a fraction of the size and usually sits in front of twMerge anyway.
class-variance-authoritynpmYou want variant-driven component APIs (size, intent) with typed props; pair it with twMerge for the final override.
tailwind-variantsnpmYou want variants plus slots and built-in conflict handling in one package instead of assembling cva with twMerge.