Use when composing Tailwind class strings dynamically across React, Vue, Svelte, Solid, or any framework where a parent component passes override classes to a child via a className prop, and the child combines them with its own defaults. Solves the duplicate-utility problem (twMerge('p-2 p-4') -> 'p-4') so the LAST value wins deterministically instead of the cascade emitting both and the outcome depending on stylesheet ordering. Prevents the ineffective-override trap (passing 'p-4' to a child whose default is 'p-2 text-blue-500' leaves BOTH padding classes in the markup, and whichever the bundler ships later wins), the stale-cache trap (calling extendTailwindMerge inside a render path on every render rebuilds the big data structure), the wrong-package-version trap (tailwind-merge v2.x is for Tailwind v3, v3.x is for Tailwind v4), and the prefix-mismatch trap (custom Tailwind prefix tw- requires configuring tailwind-merge with that prefix or merges silently return wrong output). Covers twMerge / twJoin / exten
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
The command stays on one line. Scroll horizontally to inspect it before copying.
Prefer a local copy? Download the files currently available to SkillsMP.
File Explorer
4 files
Showing SKILL.md
SKILL.md
Source instructions · Read-only preview
name
tailwind-impl-tailwind-merge
description
Use when composing Tailwind class strings dynamically across React, Vue, Svelte, Solid, or any framework where a parent component passes override classes to a child via a className prop, and the child combines them with its own defaults. Solves the duplicate-utility problem (twMerge('p-2 p-4') -> 'p-4') so the LAST value wins deterministically instead of the cascade emitting both and the outcome depending on stylesheet ordering. Prevents the ineffective-override trap (passing 'p-4' to a child whose default is 'p-2 text-blue-500' leaves BOTH padding classes in the markup, and whichever the bundler ships later wins), the stale-cache trap (calling extendTailwindMerge inside a render path on every render rebuilds the big data structure), the wrong-package-version trap (tailwind-merge v2.x is for Tailwind v3, v3.x is for Tailwind v4), and the prefix-mismatch trap (custom Tailwind prefix tw- requires configuring tailwind-merge with that prefix or merges silently return wrong output). Covers twMerge / twJoin / extendTailwindMerge / createTailwindMerge / getDefaultConfig / mergeConfigs / fromTheme / validators, the shadcn/ui cn helper pattern (clsx + twMerge), class-variance-authority (cva) integration with base + variants + compoundVariants + defaultVariants, the VariantProps TypeScript type, custom utility registration for design-system tokens, prefix support, cacheSize tuning, when to use twMerge vs twJoin vs plain string concatenation. Keywords: tailwind-merge, twMerge, twJoin, clsx tailwind, cn helper, shadcn cn, class-variance-authority, cva, VariantProps, classnames, extendTailwindMerge, createTailwindMerge, getDefaultConfig, mergeConfigs, fromTheme, validators, conflict resolution, last wins tailwind, override className prop, conditional class names react, cacheSize, prefix tailwind merge, custom utility merge, shadcn ui button, my padding class not applied, tailwind classes both appearing, override className not working, how to merge tailwind classes, react className prop, dynamic tailwind classes, cva button pattern, variant pattern react, install tailwind-merge, tailwind-merge v2, tailwind-merge v3, tailwind-merge for tailwind v4, tailwind-merge for tailwind v3, memoize class merge, render path performance
license
MIT
compatibility
Designed for Claude Code. Requires Tailwind CSS v3.4 or v4.0+.
metadata
{"author":"OpenAEC-Foundation","version":"1.0"}
tailwind-merge : Deterministic Class Override
tailwind-merge is a runtime library that takes any number of
Tailwind class strings and returns ONE string where conflicting
utilities are resolved with a LAST-WINS rule.
It is the foundation of the shadcn/ui cn() helper, the cva-based
component variant pattern, and any React/Vue/Svelte component that
accepts a className prop and combines it with internal defaults.
Companion skills :
tailwind-impl-config-v3 and tailwind-impl-config-v4 : the
Tailwind side of the equation.
tailwind-impl-plugins-official : how the official plugins emit
class groups that tailwind-merge needs to know about.
shadcn-ui skill package : the upstream design system that
popularised the cn helper.
Output markup contains p-2 bg-white p-4. The compiled CSS contains
BOTH .p-2 and .p-4. Whichever appears later in the stylesheet
wins. With JIT this is non-deterministic across builds.
Output markup contains bg-white p-4. Deterministic. p-2 is removed
because it conflicts with p-4 in the same padding class group.
Quick Reference : Install
Tailwind version
tailwind-merge version
npm install
v3.x
2.x (latest 2.6.0)
npm install tailwind-merge@2
v4.x
3.x (current)
npm install tailwind-merge@3
ALWAYS pin to the major matching your Tailwind version. v2.x knows
about v3-era class groups ; v3.x knows about v4-era class groups. A
mismatch silently produces wrong merges for utilities the library
does not recognise.
clsx resolves conditionals + arrays + objects into a single string ;
twMerge then resolves Tailwind-level conflicts.
Decision Tree : Do I Need tailwind-merge
Does the class list ever come from props or user input ?
├── NO (always static, hard-coded in JSX) -> NO twMerge needed
│ Example: <div className="p-4 bg-blue-500"> -> just write it
│
└── YES (className prop, variant logic, theme switching, etc.)
-> YES, use twMerge OR cn(clsx(...))
Are the classes conditional but never conflict ?
├── YES (e.g. show/hide a class only) -> twJoin or clsx are enough
│ Example: clsx("base", isActive && "bg-blue-500")
│
└── YES (different sizes/variants override each other)
-> twMerge IS required
Decision Tree : twMerge vs twJoin vs clsx vs cn
Tool
Conflict resolution
Conditionals
Bundle
twMerge
yes (LAST WINS)
only strings
larger (full data structure)
twJoin
no
strings + arrays + nullables
smaller
clsx
no
strings + arrays + objects + nullables
smallest
cn (shadcn)
yes
everything clsx accepts
largest
ALWAYS pick the lightest tool that does the job :
Static classes only -> just write the string
Conditional, no conflict possible -> clsx OR twJoin
Conditional, conflicts possible -> cn(clsx(...))
External override via className prop -> twMerge OR cn
Same signature, NO conflict resolution. Just concatenation with
whitespace and falsy filtering. Faster + smaller than twMerge. Use
when you know nothing will conflict.
Returns a CUSTOM twMerge function that knows about additional class
groups, theme scales, prefix, or conflict rules. ALWAYS call this
ONCE at module scope, NEVER inside a render path.
Returns a twMerge that uses ONLY your config (no inheritance from
defaults). Used when you have NO Tailwind core classes at all and a
fully bespoke utility set. Rare.
getDefaultConfig()
Returns the default config object. Use as a starting point for
mergeConfigs or to inspect what tailwind-merge knows about.
mergeConfigs(...configs)
Combines multiple configs. Useful when composing extensions from
several design-system packages.
fromTheme(key)
Returns a "theme getter" function used inside classGroups to
reference theme scales. Lets extendTailwindMerge resolve dynamic
values from the Tailwind theme.
validators
A module of helper validators for class-group patterns. Examples :
isNumber, isLength, isArbitraryValue, isArbitraryLength,
isPercent, isTshirtSize. Pass into classGroups to accept any
matching arbitrary value.
CRITICAL : Performance Rule
extendTailwindMerge builds a large internal data structure. Calling
it on every render costs milliseconds per call AND defeats the
internal LRU cache.
<Button variant="outline" size="lg" className="rounded-full px-10">
Click me
</Button>
rounded-md from the base is replaced by rounded-full because they
conflict in the border-radius group. px-6 from size: lg is
replaced by px-10. Other classes survive.
CRITICAL : When NOT to Use tailwind-merge
Static utility list with no possible conflicts ?
-> Don't use it. Just write the string.
A single condition that adds or removes one class ?
-> Use clsx or twJoin. Lighter.
Server-rendered output with NO client interaction ?
-> Consider precomputing at build time, not at request time.
Email template renderer (HTML to many clients) ?
-> tailwind-merge runs in the renderer fine. Tailwind itself does
not (no JS in email clients).
Every twMerge call has a runtime cost (cache lookup or full parse).
Avoid the hot-path tax when not needed.
Cross-Link : shadcn/ui Pattern
The shadcn/ui design system standardises ALL components on the
cn helper that wraps twMerge and clsx. If you adopt shadcn :
src/lib/utils.ts exposes cn.
Every component uses cn for its root className.
cva is used for variant-based components (Button, Badge, Alert).
See the companion shadcn-ui Claude Skill Package for the full
component set and the install scripts.
What This Skill Does NOT Cover
Writing your own Tailwind utility classes (use
tailwind-impl-config-v3 or tailwind-impl-config-v4).
The Tailwind cascade itself (use tailwind-syntax-cascade).