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-is—twMerge 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(orclassnames): 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
- Tailwind class merger —
tailwind-mergein the browser, combine or per-line - CSS clamp() and fluid type — spacing and type that scale without utility piles
- Border-radius: eight values, elliptical corners, and squircle presets
- Text case styles: camelCase, snake_case, kebab-case
- Why “local only” matters for developer tools