Open-source project
dcastil/tailwind-merge avatar
dcastil/tailwind-merge

tailwind-merge: resolving conflicting Tailwind classes in JavaScript

Merge Tailwind CSS classes without style conflicts

5,694 stars105 forksTypeScriptMIT

At a glance

What is it?
tailwind-merge is a TypeScript utility that merges Tailwind CSS class strings so the last conflicting utility wins. It is small, MIT licensed, and aimed at component authors who let callers override styles through a className prop.
Who is it for?
Adopt tailwind-merge if you build reusable components that accept a className prop and you want the caller's utilities to override the component's defaults without !important. Skip it if your class strings are static, since the merge runs on every call and there is nothing to resolve.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 18 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 3, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The className override problem tailwind-merge exists to solve

A component that hardcodes its own Tailwind classes and also accepts a className prop has a conflict problem. The two class strings are concatenated, both utilities land in the DOM, and CSS specificity decides the winner rather than the order the author intended. Tailwind's own answer is !important, which is exactly the kind of escape hatch that spreads.

tailwind-merge takes a different route. It parses the class string, groups utilities by the CSS property they affect, and keeps the last one in each group. The README's opening example shows the shape: twMerge('px-2 py-1 bg-red hover:bg-dark-red', 'p-3 bg-[#B91C1C]') returns 'hover:bg-dark-red p-3 bg-[#B91C1C]'. The px-2 and py-1 disappear because p-3 covers the same properties, and the arbitrary-value background wins over bg-red. The hover: variant survives because it is a different variant and therefore a different group.

The audience is component library authors and anyone maintaining a design system on top of Tailwind. If your class strings are written once and never composed, the library has nothing to do for you.

How the merge works and what the variants do to it

The mechanism is a class-group resolver rather than a string diff. Each Tailwind utility maps to a group key, and modifiers such as hover:, focus:, and responsive prefixes become part of that key, so a hover: utility never cancels a base utility. Within one group, the later class wins, which is why argument order matters: the second argument to twMerge is treated as the override layer.

Arbitrary values are handled as their own case. bg-[#B91C1C] is recognised as a background utility, so it competes with bg-red rather than sitting beside it. That is the part that makes the library usable with design tokens that do not map to a named Tailwind colour.

The repository is a pnpm monorepo. The library itself lives in packages/tailwind-merge, and its docs are generated into the package README from packages/tailwind-merge/docs/README.md, so the GitHub page and the npm page stay in step. Alongside it sit @tailwind-merge/vite and @tailwind-merge/next, which the README describes as doing automatic theme configuration and source-based pruning for Vite and Next.js apps, plus @tailwind-merge/configurator for generating a theme-specific merge module. None of those three has a stable release; the README states they publish dev builds under the dev tag, and the configurator is unreleased with an API the README calls unstable. Treat the core package as the product and the rest as previews.

Installing tailwind-merge and merging your first override

The package is published to npm as tailwind-merge, and the README's own examples import it in application code:

ts
import { twMerge } from 'tailwind-merge'

twMerge('px-2 py-1 bg-red hover:bg-dark-red', 'p-3 bg-[#B91C1C]')
// → 'hover:bg-dark-red p-3 bg-[#B91C1C]'

That is the whole call. Pass the component's default classes first and the incoming classes second, and the second argument wins where the two touch the same CSS properties. The result is a plain string you hand to a className attribute.

The README does not print an install command; it links to the npm package page, and the package is installed from npm under the name tailwind-merge. The README does state which Tailwind versions this release line covers: Tailwind v4.0 up to v4.3, with Tailwind v3 users directed to tailwind-merge v2.6.0. Check that range before picking a version, because the group definitions follow Tailwind's utility set.

The repository's root package.json pins [email protected] and requires a Node runtime of >=22.18.0, but that governs building the monorepo from source, not consuming the published package.

Where tailwind-merge stops being the right tool

The library resolves conflicts it can recognise. A utility it does not know about, whether from a plugin, a custom @utility rule, or a class name that only looks like a Tailwind utility, is not part of any group and will not be merged away. The README links a limitations page, which is the honest place to check before assuming full coverage.

Version coupling is the second constraint. Tailwind v4.0 through v4.3 is the supported range for this release line, and Tailwind v3 requires the older v2.6.0 package. A project that upgrades Tailwind ahead of tailwind-merge, or sits on a version the current line does not cover, is the wrong fit until the supported range catches up.

The third case is static markup. If a component's classes never vary, calling twMerge adds work at runtime for a string that was already correct. The README points to Bundlephobia for the size, which is the number to check if you are weighing this against a build-time approach.

tailwind-merge vs clsx, cn, and classnames

clsx and classnames solve a different half of the problem. They conditionally assemble class strings: given an object or array of truthy values, they produce a joined string. They do not know what px-2 means, so passing both px-2 and p-3 through clsx yields both classes and leaves the conflict to CSS.

The common pairing is to use both, and the search data around this project reflects that: clsx handles the conditional logic, twMerge handles the conflict. That combination is what the cn helper in shadcn-style codebases usually is, a thin wrapper that calls clsx and then twMerge.

The difference from a variants library is one of scope. A variants tool maps named props to class strings; tailwind-merge does not define variants at all, it only arbitrates between utilities that are already in the string. If you need prop-to-class mapping, tailwind-merge is a layer underneath that, not a replacement.

Maintenance, releases, and the MIT licence

The repository is not archived, and the last push was on 2026-09-15. The most recent release, [email protected], is dated 2026-09-12, following v3.6.0 in May 2026 and v3.5.0 in February 2026. That is a steady cadence rather than a burst, and the versioning page linked from the README is the place to read what the project promises between minor releases.

The monorepo is built with pnpm, and the root package.json pins [email protected] with a Node runtime requirement of >=22.18.0. That matters only if you plan to build from source; consumers installing from npm are not affected by the workspace tooling.

The licence is MIT, declared both in the root package.json and in the repository's LICENSE.md. MIT permits commercial use and modification with the copyright notice retained. That is a description of the licence text, not legal advice; if your organisation has a policy on attribution or on vendoring dependencies, run the actual terms past whoever owns that policy.

Editorial conclusion

Adopt tailwind-merge if you build reusable components that accept a className prop and you want the caller's utilities to override the component's defaults without !important. Skip it if your class strings are static, since the merge runs on every call and there is nothing to resolve. Before rolling it out, check the Tailwind version in your project against the supported range (v4.0 through v4.3, with v2.6.0 for Tailwind v3) and read the limitations page for the cases the merger cannot resolve.

Frequently asked questions

What is tailwind-merge used for?

It merges Tailwind CSS class strings in JavaScript so that conflicting utilities do not both end up in the DOM. The README's example shows twMerge('px-2 py-1 bg-red hover:bg-dark-red', 'p-3 bg-[#B91C1C]') returning 'hover:bg-dark-red p-3 bg-[#B91C1C]'.

How do I install tailwind-merge?

It is published to npm under the name tailwind-merge, and the README links the npm package page for installation. The README's examples then import twMerge from 'tailwind-merge' in application code.

What is the difference between tailwind-merge and clsx?

clsx assembles class strings from conditional values but does not understand Tailwind utilities, so px-2 and p-3 passed through it both survive. tailwind-merge knows which utilities affect the same CSS properties and keeps the last one, which is why the two are often used together.

Is tailwind-merge a dev dependency?

The README imports twMerge directly in application code, which makes it a runtime dependency rather than a dev dependency. Its published type declarations are compatible with TypeScript 3.8 and newer.

What is tailwind-merge vs classnames?

classnames joins class strings conditionally but has no knowledge of Tailwind's utility groups, so it cannot resolve a conflict between px-2 and p-3. tailwind-merge does not replace that conditional joining; it resolves conflicts among utilities that are already in the string.

What does tailwind-merge do?

It merges Tailwind CSS classes in JS without style conflicts, keeping the last class in each group of utilities that affect the same CSS properties. Its published declarations are compatible with TypeScript 3.8 and newer.

Official sources

  1. dcastil/tailwind-merge on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/dcastil-tailwind-merge.svg)](https://hysenlabs.com/projects/dcastil-tailwind-merge)