Copy
Trading Bots
Events
More

Removing tailwind-merge from production

2026/07/18 07:06Browse 0

A developer removed tailwind-merge from all seven apps in a monorepo after discovering it silently dropped custom utility classes. The change eliminated roughly 25KB of minified JavaScript per app and uncovered two previously hidden CSS conflicts, including one that became a live regression when the runtime merger was removed.

The problem with silent class conflicts

The developer found that tailwind-merge was silently removing custom utilities like `border-grid` because it classified them as border colors. When two border color classes appeared in the same string, the later one won, and the custom utility vanished from the DOM without any warning or error. This behavior is by design: tailwind-merge parses every class string at runtime, classifies tokens against its internal tables, and removes conflicting classes. The developer realized that a class conflict is a static mistake in source code, not a runtime condition, and running a resolver in production to fix it is like shipping a spell checker inside a published book.

Another example showed `text-sm leading-snug text-xs` producing only `text-xs`, losing the line height. tailwind-merge treats font size as conflicting with line height because `text-sm` can set both. The result was defensible but surprising, and again completely silent.

Replacing runtime merging with compile-time checks

The developer replaced tailwind-variants with a simple concatenation helper and made every variant string disjoint: no property set by both the base and a variant for the same modifier prefix. Contested tokens moved out of the base and into every variant that does not override them. Before the rewrite, tailwind-merge's output for all 109 variant combinations was captured as a baseline. After the rewrite, a test asserts that the output matches the baseline exactly and that running twMerge over it changes nothing. That test stays in the repo permanently to catch new shadcn components written in merge semantics.

For call sites where overrides are intentional, the developer used Tailwind's important modifier (`rounded-full!`) instead of relying on tailwind-merge. A script resolved each component's actual classes, diffed every class literal against what twMerge would drop, and rewrote the conflicting tokens at 233 call sites. Each site was pixel-identical because the rewrite target was the string twMerge was already producing.

The new tool: overrule

The developer extracted the safety net into a package called overrule. Its `cross` command judges every class literal twice: once with tailwind-merge's name tables and once with the actual compiled stylesheet, printing every place the two disagree. The stylesheet side compiles classes with Tailwind itself to avoid drift. When pointed back at the seven apps, it found only one disagreement: a bare `filter` token sitting dead next to `blur-[10px]`, invisible to name tables because every filter utility restates the full filter chain.

With the variants disjoint and callers explicit, `cn` became a plain join, and tailwind-merge moved into a dev-only tripwire that names conflicting tokens the moment they render. Bundlers eliminate the whole branch in production. The developer also found two conflicts on pages nobody had rendered in dev: a marketing page where `py-12` was fighting `p-10`, and a vendored component carrying both `outline-hidden` and `outline-none`. The checker caught what the migration missed.

Disclaimer: This page may contain third-party information and does not necessarily reflect BYDFi's views or opinions. This content is for general reference only and does not constitute any representation, warranty, financial advice, or investment advice. BYDFi is not responsible for any errors, omissions, or any results arising from the use of such information. Virtual asset investments involve risks. Please carefully evaluate the risks of the product and your risk tolerance based on your financial situation. For more information, please refer to our Terms of Use and Risk Disclosure.