Frequently Asked Questions (FAQ)
v3 Migration
What's new in v3?
v3 introduces several major improvements:
- Two build options: Original (with tailwind-merge) and Lite (without tailwind-merge)
- Massive performance improvements: Up to 500x faster when using tailwind-merge
- Enhanced
cnutility: Now supports functionality similar toclassnames/clsx - New
cxfunction (v3.2.0+): Lightweight utility without conflict resolution cndefaults to conflict resolution (v3.2.0+):cnnow usestailwind-mergeby default in the original build
Should I use the original build or lite build?
Use the original build if:
- You need automatic Tailwind CSS conflict resolution
- You're already using tailwind-merge in your project
- Performance with conflict resolution is important (v3 is 400-500x faster)
Use the lite build if:
- Bundle size is critical (~80% smaller)
- You don't need automatic conflict resolution
- You handle class merging manually
How do I migrate from v2 to v3?
For most users, just ensure tailwind-merge is installed:
npm install tailwind-mergeIf you want to use the lite build instead:
// Change your imports from:
import { tv } from 'tailwind-variants';
// To:
import { tv } from 'tailwind-variants/lite';What happened to lazy loading in v3?
v3 removes lazy loading of tailwind-merge in favor of two separate builds. This fixes issues where classes wouldn't merge correctly on first render. The original build now statically imports tailwind-merge, while the lite build doesn't include it at all.
Should I replace cnBase with cx? (v3.2.0+)
Yes! In v3.2.0+, cnBase should be replaced with cx. The cx function provides the same functionality (simple concatenation without conflict resolution) with a clearer name.
Migration example:
// Before (v3.1.1 and earlier)
import { cnBase } from 'tailwind-variants';
{cnBase("flex items-center justify-center gap-2", className)}
// After (v3.2.0+)
import { cx } from 'tailwind-variants';
{cx("flex items-center justify-center gap-2", className)}The API is identical - both functions combine class names without conflict resolution. cnBase is still exported for backwards compatibility, but cx is the recommended replacement.
What's the difference between cx, cn, and cnMerge? (v3.2.2+)
There are three utility functions for concatenating class names, each serving different purposes:
cx - Lightweight utility without conflict resolution:
- Available in both original and lite builds
- Simply concatenates class names without merging conflicts
- Smaller bundle size
- Use when you don't need conflict resolution
import { cx } from 'tailwind-variants';
cx('text-blue-500', 'text-red-500'); // => "text-blue-500 text-red-500" (both included)cn - Full-featured utility with conflict resolution (original build only):
- Uses
tailwind-mergefor automatic conflict resolution - Returns a string directly (v3.2.2+)
- Resolves conflicting Tailwind classes intelligently with default config
- Use for the common case when you need conflict resolution
import { cn } from 'tailwind-variants';
cn('text-blue-500', 'text-red-500'); // => "text-red-500" (conflict resolved)cnMerge - Configurable utility with conflict resolution (original build only):
- Supports custom twMerge configuration via second function call
- Use when you need to customize twMerge behavior (disable it or provide custom config)
import { cnMerge } from 'tailwind-variants';
cnMerge('px-2', 'px-4')({ twMerge: false }); // => "px-2 px-4" (no conflict resolution)
cnMerge('px-2', 'px-4')(); // => "px-4" (with default config)When to use each:
- Use
cxwhen you don't need conflict resolution and want a smaller bundle - Use
cnwhen you need automatic Tailwind CSS conflict resolution with default config (most common case) - Use
cnMergewhen you need to customize twMerge behavior (disable it or provide custom config)
v2 Migration
Why is tailwind-merge now optional?
In v2, we made tailwind-merge an optional peer dependency to give users more control over their bundle size. Not all projects need automatic conflict resolution, and by making it optional, users who don't need this feature can save significant bundle size. The library itself is now up to 80% smaller - only 5.5KB minified (2.1KB gzipped), compared to v1's 28.3KB minified (9.2KB gzipped).
Do I need to install tailwind-merge separately?
Yes, if you want to use the automatic conflict resolution feature (which is enabled by default), you need to install tailwind-merge as a peer dependency:
npm install tailwind-mergeIf you don't need conflict resolution, you can disable it by setting twMerge: false in your config.
What happens if I don't install tailwind-merge but keep twMerge enabled?
Your application will throw an error at runtime when trying to use tailwind-variants. You must either:
- Install
tailwind-mergeas a dependency, or - Set
twMerge: falsein your configuration
How much faster is v2 compared to v1?
v2 is 37-62% faster for most operations. The performance improvements come from:
- Optimized object creation and array operations
- Reduced function call overhead
- Better memory management
- More efficient class merging algorithms
Common Issues
Why are my Tailwind classes not being applied correctly?
This usually happens when:
- Conflict resolution is disabled: Make sure you have
tailwind-mergeinstalled andtwMergeis set totrue(default) - Class order matters: Later classes in your variants override earlier ones
- Specificity issues: Custom classes passed via
class/classNameprop will override variant classes
How do I override styles from a variant?
You can override styles in several ways:
// Using the class prop
button({ color: 'primary', class: 'bg-pink-500' })
// Using compound variants
tv({
variants: { /* ... */ },
compoundVariants: [
{
color: 'primary',
size: 'large',
class: 'bg-pink-500'
}
]
})Why is TypeScript not inferring my variant types correctly?
Make sure you're using as const for your variant definitions if you're defining them outside of the tv function:
const variants = {
color: {
primary: 'bg-blue-500',
secondary: 'bg-gray-500'
}
} as const;
const button = tv({ variants });Performance
Should I disable twMerge for better performance?
While disabling twMerge can provide a small performance boost, we recommend keeping it enabled unless:
- You're building a performance-critical application
- You're confident you won't have class conflicts
- You're already handling class merging manually
How can I optimize my tailwind-variants usage?
- Reuse tv instances: Create tv components once and reuse them
- Avoid dynamic class generation: Define all variants upfront rather than generating classes dynamically
- Use compound variants wisely: They add overhead, so use them only when necessary
// Good - defined once
const button = tv({ /* ... */ });
// Avoid - creates new instance each render
function Component() {
const button = tv({ /* ... */ });
}Usage Patterns
Can I use tailwind-variants without React?
Yes! Tailwind Variants is framework agnostic. It works with any JavaScript framework or vanilla JavaScript:
// Vanilla JS
const button = tv({ /* ... */ });
document.getElementById('button').className = button({ color: 'primary' });// Vue
<template>
<button :class="button({ color: 'primary' })">Click me</button>
</template>// Svelte
<button class={button({ color: 'primary' })}>Click me</button>How do I handle responsive variants?
You can use Tailwind's responsive prefixes directly in your variant definitions:
const component = tv({
base: 'text-sm md:text-base lg:text-lg',
variants: {
color: {
primary: 'text-blue-500 md:text-blue-600',
secondary: 'text-gray-500 md:text-gray-600'
}
}
});Can I extend multiple components?
No, you can only extend one component at a time. However, you can compose multiple components manually:
const baseButton = tv({ /* ... */ });
const iconButton = tv({ /* ... */ });
// Compose manually
const combinedButton = tv({
base: [baseButton(), iconButton()],
// additional variants...
});How do I use slots with TypeScript?
When using slots with TypeScript, you can extract the slot functions for better type inference:
const card = tv({
slots: {
base: 'p-4',
title: 'text-lg font-bold',
content: 'text-sm'
}
});
// Extract with types
const { base, title, content } = card();
// Or use directly
<div className={card().base()}>
<h1 className={card().title()}>Title</h1>
</div>Still have questions?
If you have any questions not covered here, please ask in the Discord (opens in a new tab) or open a GitHub Discussion (opens in a new tab).