Skip to Content

FAQ

Answers to common questions about licensing, default vs lite builds, tailwind-merge, slots, TypeScript, and frameworks.

Is Tailwind Variants free?

Yes. MIT licensed. Use it in commercial and open-source projects.

Default build or lite?

Default (tailwind-variants) when you want automatic Tailwind conflict resolution. Included since v3.3, no extra install.

Lite (tailwind-variants/lite) when you do not need the table, or you already have twMerge.

import { tv } from 'tailwind-variants';       // built-in merge
import { tv } from 'tailwind-variants/lite';  // no table

Inject a merger on lite with createTV / createCN. See Configuration.

Do I need tailwind-merge?

No, not for the default build on v3.3+. Merge is included.

Keep tailwind-merge if your app imports it directly (twMerge, extendTailwindMerge, etc.). On /lite, inject that function with createTV / createCN.

Why slots: {}?

An explicit empty slots object enables slot mode with a single implicit base slot:

const x = tv({ slots: {}, base: 'p-4' });
x().base(); // slot function

const y = tv({ base: 'p-4' });
y(); // plain string — omit slots entirely

Omit slots when you do not need slot functions.

cn vs cx vs cnMerge?

Merge conflictsReturnsBuild
cxNoStringDefault + lite
cnYes (default config)StringDefault
createCNYes (injected)StringLite
cnMergeYes (custom config)CurriedDefault
cx('text-blue-500', 'text-red-500');  // both classes
cn('text-blue-500', 'text-red-500');  // "text-red-500"
cnMerge('px-2', 'px-4')({ twMerge: false }); // "px-2 px-4"

Use cn for everyday merge. Use cx when classes cannot conflict. On lite, use createCN to inject a merger. Use cnMerge for per-call config.

Framework support?

Tailwind Variants is framework-agnostic. It returns class strings — use with any framework or vanilla JS. See Quick Start for binding examples.

Can I extend multiple components?

Yes. Pass a non-empty array and parents merge left to right:

const button = tv({ extend: [baseButton, focusRing, pressable] });

On earlier versions, chain extends or compose class strings manually. See Extending.

Why did a class disappear from the output?

Conflict resolution probably merged it away: the last conflicting utility wins (Class Resolution). To see exactly which classes were dropped, enable the experimental debug mode in development:

const button = tv({ /* ... */ }, { debug: true });

TypeScript not inferring variants?

Define variants inline inside tv, or use as const on external objects:

const variants = { primary: 'bg-zinc-900 text-white', secondary: 'bg-zinc-100' } as const;
const button = tv({ variants: { variant: variants } });

Still stuck?

Ask in Discord or open a GitHub Discussion.

On this page