Merging Tailwind className strings: tailwind-merge and conflict groups

Published: 2026-09-05

How tailwind-merge resolves conflicting utilities (padding, colors, variants), why last-wins beats string concat, and how to clean className strings locally.

In Tailwind CSS, utilities are single-purpose classes. px-4 sets horizontal padding; p-3 sets padding on all sides. If both appear on the same element, CSS cascade and source order decide the winner—often not the class you meant. Concatenating strings with template literals (\${base} ${extra}``) keeps every token, so conflicting pairs pile up in the DOM and in your mental model.

tailwind-merge (twMerge) solves that: it knows Tailwind’s conflict groups, drops earlier utilities that lose to later ones, and returns a single cleaned class string. LocalTools’ Tailwind class merger runs the same library in the browser—paste base + overrides, copy the merged result, nothing uploaded.

Why plain concat fails

const base = 'px-4 py-2 bg-red-500 text-sm';
const override = 'p-3 bg-red-600 text-base';
// ❌ Both padding and both background utilities remain
className={`${base} ${override}`}

In the stylesheet, p-3 and px-4 touch overlapping properties. Depending on generated CSS order, you can get surprising edges (left/right from one rule, top/bottom from another). Duplicate intent also bloats HTML and confuses reviews: “which padding is real?”

A merge step makes intent explicit: later tokens win within a conflict group, like prop overrides in a component API.

Conflict groups (mental model)

tailwind-merge maps utilities into groups that cannot sensibly coexist. Rough categories:

Group idea Example conflict Typical winner
Padding (all / axis / side) p-3 vs px-4 vs pt-2 Last token that covers the same sides
Margin m-4 vs mx-2 Same last-wins rules
Sizing w-full vs w-1/2 Last width utility
Colors bg-red-500 vs bg-blue-600 Last background color
Typography text-sm vs text-base Last font-size
Border radius rounded-lg vs rounded-xl Last radius scale

Exact grouping follows Tailwind’s design (and the merge library’s default config for Tailwind v3-style utilities). You do not need to memorize every group—treat “same CSS concern” as the rule of thumb.

Variants are part of the key. hover:bg-blue-500 does not fight bg-red-500 (different state). md:px-6 does not remove base px-4 (different breakpoint). Conflicts happen when two classes target the same property set under the same variant stack.

px-4 py-2 bg-red-500 hover:bg-blue-500
p-3 bg-red-600

Merged (combine mode, later wins): padding collapses toward p-3, background toward bg-red-600, and hover:bg-blue-500 remains because it is a different variant.

Arbitrary values and custom classes

Arbitrary values (px-[13px], bg-[#1a1a1a]) participate in the same groups as their scale peers when the library recognizes the prefix. Unknown or fully custom class names (app-specific helpers, third-party CSS modules) are usually kept as-istwMerge does not invent CSS; it only drops known conflicting Tailwind utilities.

If your app uses a custom Tailwind prefix or heavily extended theme, app code may need a configured extendTailwindMerge. The LocalTools merger uses default conflict groups—good for stock and common utilities, not a substitute for a project-specific cn helper when your theme is exotic.

The cn / clsx + twMerge pattern

In React apps the usual helper is:

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

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
  • clsx (or classnames): joins conditionals, arrays, and objects into one string.
  • twMerge: resolves Tailwind conflicts after the join.

Order still matters: put defaults first, caller className last, so overrides win.

<button className={cn('px-4 py-2 bg-indigo-600 text-white', className)} />

Cleaning strings outside the app

Sometimes you are not editing the cn helper—you are reviewing a pasted className from Storybook, a design handoff, or a bloated DOM dump. The Tailwind class merger supports:

Mode Behavior
Combine all lines Join non-empty lines into one token stream, then merge (base + override lines)
Merge each line Run merge independently per line (batch cleanup of many class strings)

Live preview applies the merged classes to a sample element so you can spot obvious mistakes before copying. Stats show how many utilities were removed as conflicts or duplicates.

Related local workflows: normalize spacing with the Whitespace normalizer, or compare before/after with Diff Checker. For case-style renames in identifiers (not Tailwind scales), see Text case styles.

Why merge locally

Class strings rarely contain secrets, but they often encode unreleased UI, internal component APIs, or client-specific themes. Pasting them into a random online merger is unnecessary exposure. Running tailwind-merge in the tab matches the site’s local-only model.

Try it locally

Open Tailwind class merger, paste utilities (one string or base + override on separate lines), choose Combine all lines or Merge each line, then copy the merged output when the removed-count looks right.

Related reading

All learn articles