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.
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.
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
- You control every class combination statically: if no outside code injects competing classes, clsx or template strings do the job without this package's roughly 9 kB of gzipped config and logic (bundlephobia)
- You customized Tailwind theme scales beyond colors: tailwind-merge never reads your CSS or Tailwind config, so every custom spacing or text size must be manually mirrored via extendTailwindMerge, and the two drift silently when someone edits one and not the other
- You are on Tailwind CSS v3: the current 3.x line only supports Tailwind v4, so you are pinned to tailwind-merge 2.6.0 and its older docs
- Your real problem is component variants: if you reach for twMerge to juggle size and intent props, class-variance-authority or tailwind-variants model that directly and reduce how often merging is needed at all
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 twMergetwMerge 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.0tailwind-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
| Package | Registry | Pick it when |
|---|---|---|
| clsx | npm | You 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-authority | npm | You want variant-driven component APIs (size, intent) with typed props; pair it with twMerge for the final override. |
| tailwind-variants | npm | You want variants plus slots and built-in conflict handling in one package instead of assembling cva with twMerge. |