# Acknowledgements (https://www.tailwind-variants.org/docs/acknowledgements)

Credits to the people and open-source projects that inspired and shaped Tailwind Variants.

**Tailwind Variants** is built by the [HeroUI](https://heroui.com) team and inspired by the variant-system work that came before it.

## Stitches
[Stitches](https://stitches.dev) by [Modulz](https://modulz.app) pioneered the variants API pattern that Tailwind Variants builds on.

## CVA
[CVA](https://cva.style) by [Joe Bell](https://github.com/joe-bell) is an excellent tool for type-safe Tailwind variants. Tailwind Variants started as an extension of that idea — adding slots, built-in merge, and design-system composition.

If you only need a lightweight variant helper, we recommend CVA. See the [Comparison](https://www.tailwind-variants.org/docs/comparison) guide for a feature breakdown.

## Conflict resolution
Merge logic in the default build draws on ideas and MIT-licensed work from:

* [tailwind-merge](https://github.com/dcastil/tailwind-merge) — Tailwind class conflict resolution
* [clsx](https://github.com/lukeed/clsx) — conditional class concatenation
* [cnfast](https://github.com/aidenybai/cnfast) — fast `cn` utilities

## Community
Thanks to everyone who filed issues, opened PRs, and shared feedback on [GitHub](https://github.com/heroui-inc/tailwind-variants) and [Discord](https://discord.gg/9b6yyZKmH4).

# API Reference (https://www.tailwind-variants.org/docs/api-reference)

Reference for tv, createTV, VariantProps, cn/cx/cnMerge, and configuration types exported by Tailwind Variants.

Condensed reference for `tailwind-variants` v3.3.

```ts
import {
  tv,
  createTV,
  cn,
  cnMerge,
  cx,
  type VariantProps
} from 'tailwind-variants';
```

Lite build:

```ts
import { tv, cx } from 'tailwind-variants/lite';
```

## `tv`
Creates a variant recipe. Returns a callable function plus metadata.

```ts
const button = tv(options, config?);

button({ variant: 'primary' }); // => string (no slots)
button({ variant: 'primary' }).base(); // => string (with slots)

button.base;              // resolved base string
button.slots;             // slot class map
button.variants;          // variant definitions
button.variantKeys;       // ['variant', 'size', ...]
button.defaultVariants;   // { variant: 'primary', ... }
button.compoundVariants;  // array
button.compoundSlots;     // array
```

### Options
```ts
type TVOptions = {
  extend?: TVReturnType;
  base?: ClassValue;
  slots?: Record<string, ClassValue>;
  variants?: Record<string, Record<string, ClassValue>>;
  defaultVariants?: Record<string, ClassValue>;
  compoundVariants?: Array<Record<string, unknown> & ClassProp>;
  compoundSlots?: Array<Record<string, unknown> & ClassProp>;
};
```

| Option             | Description                                                       |
| ------------------ | ----------------------------------------------------------------- |
| `extend`           | Merge another recipe's base, slots, variants, defaults, compounds |
| `base`             | Shared classes for every call                                     |
| `slots`            | Named parts; `{}` enables implicit `base` slot only               |
| `variants`         | Variant axes; per-slot objects when using slots                   |
| `defaultVariants`  | Values applied when keys are omitted                              |
| `compoundVariants` | Extra classes when multiple conditions match                      |
| `compoundSlots`    | Extra classes on specific slots when conditions match             |

### Config (2nd argument)
```ts
type TVConfig = {
  twMerge?: boolean;
  twMergeConfig?: TwMergeConfig;
};
```

### Call-site props
```ts
type ClassProp = {
  class?: ClassValue;
  className?: ClassValue;
};
```

Pass variant keys plus optional `class` / `className` overrides.

## `createTV`
Returns a `tv` with default config:

```ts
const tv = createTV({
  twMerge: true,
  twMergeConfig: { extend: { /* ... */ } }
});
```

## `cn`
Concatenate and merge with default config. Returns a **string** (v3.2.2+).

```ts
cn('px-2', 'px-4'); // => "px-4"
cn('text-sm', { 'font-bold': true }); // => "text-sm font-bold"
```

Default build only.

## `cnMerge`
Concatenate with optional per-call config:

```ts
cnMerge('px-2', 'px-4')(); // => "px-4"
cnMerge('px-2', 'px-4')({ twMerge: false }); // => "px-2 px-4"
```

Default build only. Not exported from `/lite`.

## `cx`
Lightweight concat without conflict resolution:

```ts
cx('text-blue-500', 'text-red-500'); // => "text-blue-500 text-red-500"
```

Available in default and lite builds. Replaces deprecated `cnBase`.

## `VariantProps`
Extract variant prop types from a recipe:

```ts
type Props = VariantProps<typeof button>;
// { variant?: 'primary' | 'secondary' | 'tertiary'; size?: 'sm' | 'md' }
```

## Types
| Type              | Description                                                          |
| ----------------- | -------------------------------------------------------------------- |
| `ClassValue`      | `string \| string[] \| Record<string, boolean> \| null \| undefined` |
| `VariantProps<T>` | Inferred variant keys from recipe `T`                                |
| `TVReturnType`    | Return type of `tv()` — callable + metadata                          |

## Slotted return type
When `slots` is defined, calling the recipe returns an object of functions:

```ts
const { base, title } = alert({ color: 'danger' });
base({ class: 'mt-2' }); // => string
```

Each slot function accepts `class` / `className` and variant overrides scoped to that slot.

# Class Resolution (https://www.tailwind-variants.org/docs/class-resolution)

How Tailwind Variants resolves and merges conflicting utilities with cn, cx, and cnMerge in the default and lite builds.

Tailwind Variants ships utilities for joining class strings. Pick the one that matches whether you need conflict resolution and how much bundle you can spend.

## `cn` — merge with defaults
In v3.2.2+, `cn` returns a **string directly** with the default merge config. No second function call.

```ts
import { cn } from 'tailwind-variants';

cn('px-2 py-1', 'px-4');           // => "py-1 px-4"
cn('text-blue-500', 'text-red-500'); // => "text-red-500"
cn('text-sm', { 'font-bold': true }); // => "text-sm font-bold"
```

Use `cn` for the common case — concatenating classes with automatic Tailwind conflict resolution.

Available on the **default build only**. Not exported from `/lite`.

## `cx` — lightweight concat
`cx` joins classes without resolving conflicts. Same role as `clsx` — smaller and faster when merge is unnecessary.

```ts
import { cx } from 'tailwind-variants';
// or
import { cx } from 'tailwind-variants/lite';

cx('text-blue-500', 'text-red-500'); // => "text-blue-500 text-red-500"
cx(['px-4', 'py-2'], { hidden: false }); // => "px-4 py-2"
```

Use `cx` inside lite builds, static strings with no overlap, or when you handle conflicts yourself.

## `cnMerge` — custom merge config
When you need per-call control over merge behavior, use `cnMerge`. It returns a function that accepts config:

```ts
import { cnMerge } from 'tailwind-variants';

cnMerge('px-2', 'px-4')(); // => "px-4" (default merge)

cnMerge('px-2', 'px-4')({ twMerge: false }); // => "px-2 px-4"

cnMerge('px-2', 'px-4')({
  twMerge: true,
  twMergeConfig: {
    extend: {
      classGroups: {
        'font-size': ['text-tiny']
      }
    }
  }
});
```

> **info:** If you used `cn(...)(config)` before v3.2.2, migrate to `cnMerge`. `cn` no longer accepts config.

## Default vs lite
| Utility   | Default build            | Lite build               |
| --------- | ------------------------ | ------------------------ |
| `cn`      | String, with merge       | Curried no-merge adapter |
| `cnMerge` | Custom merge config      | Not exported             |
| `cx`      | Concat only              | Concat only              |
| `tv`      | Merge enabled by default | No merge                 |

Import from `tailwind-variants` for merge. Import from `tailwind-variants/lite` when bundle size is the priority.

## `twMerge` on `tv`
The second argument to `tv` accepts config:

```ts
const button = tv(
  { base: 'px-2', variants: { size: { lg: 'px-4' } } },
  { twMerge: true, twMergeConfig: { /* ... */ } }
);
```

See [Configuration](https://www.tailwind-variants.org/docs/configuration) for shared defaults via `createTV`.

# Comparison (https://www.tailwind-variants.org/docs/comparison)

Compare Tailwind Variants with Class Variance Authority (CVA), Windstitch, and hand-written class strings for component libraries.

## Feature matrix
| Feature                  | Tailwind Variants | CVA                  | Windstitch | Classnames |
| ------------------------ | ----------------- | -------------------- | ---------- | ---------- |
| Variants & defaults      | Yes               | Yes                  | Yes        | —          |
| Compound variants        | Yes               | Yes                  | Yes        | —          |
| Framework agnostic       | Yes               | Yes                  | —          | Yes        |
| Slots (split components) | Yes               | —                    | —          | —          |
| Compound slots           | Yes               | —                    | —          | —          |
| Overrides                | Yes               | Yes                  | Yes        | —          |
| `extend` composition     | Yes               | —                    | Yes        | —          |
| TypeScript autocomplete  | Yes               | Yes                  | Yes        | —          |
| Requires Tailwind CSS    | Yes               | —                    | Yes        | —          |
| Conflict resolution      | Built-in          | Via `tailwind-merge` | —          | —          |

## vs manual class strings
|               | Manual strings               | Tailwind Variants              |
| ------------- | ---------------------------- | ------------------------------ |
| Consistency   | Easy to drift across files   | Single recipe, many call sites |
| Overrides     | String concat, merge bugs    | Built-in conflict resolution   |
| Multi-part UI | Separate strings per element | Slots with per-slot variants   |
| Types         | None                         | `VariantProps` inference       |

## vs CVA (Class Variance Authority)
[CVA](https://cva.style) is an excellent, lightweight variant generator. Tailwind Variants started as an extension of that idea.

**Choose CVA** when you want the smallest possible variant helper and are fine wiring `tailwind-merge` yourself.

**Choose Tailwind Variants** when you want slots, built-in merge, compound slots, and a design-system-oriented API — especially for component libraries like [HeroUI](https://heroui.com).

# Compound Variants (https://www.tailwind-variants.org/docs/compound-variants)

Apply extra classes when multiple variant keys match — for example a specific color and size combination.

Sometimes a single axis is not enough. Compound variants add classes when **multiple** variant conditions are true at once — like `variant: primary` and `size: lg` together.

## Basic compound variant
Each entry lists the conditions and the classes to apply. Use `class` or `className` — they are equivalent:

```ts
import { tv } from 'tailwind-variants';

const button = tv({
  base: 'inline-flex cursor-pointer items-center justify-center rounded-full font-medium select-none transition-colors',
  variants: {
    variant: {
      primary: 'bg-zinc-900 text-white',
      secondary: 'border border-zinc-300 bg-zinc-50 text-zinc-900',
      tertiary: 'text-zinc-700'
    },
    size: {
      sm: 'h-8 px-3 text-sm',
      lg: 'h-11 px-6 text-base'
    }
  },
  compoundVariants: [
    {
      variant: 'primary',
      size: 'lg',
      class: 'shadow-lg shadow-zinc-900/20'
    },
    {
      variant: 'secondary',
      size: 'lg',
      className: 'shadow-md shadow-zinc-900/10'
    }
  ]
});
```

Large primary and secondary buttons pick up a soft shadow from the compound rules. The small primary and tertiary buttons do not.

## Boolean conditions
Compound variants work with boolean axes:

```ts
const button = tv({
  variants: {
    variant: {
      primary: 'bg-zinc-900 text-white',
      secondary: 'bg-zinc-100 text-zinc-900'
    },
    flat: { true: 'bg-transparent shadow-none', false: '' }
  },
  compoundVariants: [
    {
      variant: 'primary',
      flat: true,
      class: 'bg-zinc-900/10 text-zinc-900'
    }
  ]
});
```

## When to use them
Use compound variants for styles that depend on more than one axis at once — for example primary + large, or flat + primary. Keep styles that belong to a single axis on that axis.

# Configuration (https://www.tailwind-variants.org/docs/configuration)

Share defaults across recipes with createTV, tweak defaultConfig globally, and customize conflict-resolution merge config.

Configure merge behavior once, then reuse it across every recipe in your design system.

## Global `defaultConfig`
To change settings for every `tv` call, mutate the exported `defaultConfig` value (v3.2+):

```ts
import { defaultConfig } from 'tailwind-variants';

defaultConfig.twMerge = false;
```

Prefer `createTV` when only some recipes should share a config. Mutating `defaultConfig` affects the whole process — useful for app-wide defaults, easy to overreach in libraries.

## `createTV`
`createTV` returns a `tv` function with baked-in config:

```ts
import { createTV } from 'tailwind-variants';

const tv = createTV({
  twMerge: true,
  twMergeConfig: {
    extend: {
      classGroups: {
        'font-size': ['text-tiny', 'text-small']
      }
    }
  }
});

const button = tv({
  base: 'font-medium',
  variants: {
    size: {
      tiny: 'text-tiny',
      small: 'text-small'
    }
  }
});
```

Every recipe created with this `tv` inherits the same merge settings.

## `twMergeConfig`
Extend or override the built-in merge groups when you use custom Tailwind tokens:

```ts
import { createTV } from 'tailwind-variants';

const tv = createTV({
  twMergeConfig: {
    extend: {
      theme: {
        spacing: ['gap-grid']
      },
      classGroups: {
        gap: [{ gap: ['grid'] }]
      }
    },
    override: {
      // replace default groups when needed
    }
  }
});
```

Prefer `{ extend, override }` over replacing the entire config. Existing `twMergeConfig` objects from tailwind-merge generally keep working.

## Per-recipe config
Pass config as the second argument to `tv` for one-off overrides:

```ts
import { tv } from 'tailwind-variants';

const badge = tv(
  { base: 'rounded-full px-2', variants: { /* ... */ } },
  { twMerge: false }
);
```

Per-recipe config wins over `createTV` defaults for that instance.

## Lite build
`createTV` works with both entry points:

```ts
import { createTV } from 'tailwind-variants/lite';

const tv = createTV({ twMerge: false });
```

The lite build ignores merge either way — use it when config is about API consistency, not merge.

# Default Variants (https://www.tailwind-variants.org/docs/default-variants)

Set sensible default variant values on a recipe so call sites stay short, then override them only when needed.

Defaults apply when a variant key is omitted at the call site. Pass an explicit value to override any default.

## Setting defaults
Use `defaultVariants` to pick the variant value applied when a key is omitted:

```tsx
import { tv } from 'tailwind-variants';

const button = tv({
  base: 'inline-flex cursor-pointer items-center justify-center rounded-full font-medium select-none transition-colors',
  variants: {
    variant: {
      primary: 'bg-zinc-900 text-white',
      secondary: 'border border-zinc-300 bg-zinc-50 text-zinc-900',
      tertiary: 'text-zinc-700'
    },
    size: {
      sm: 'h-8 px-3 text-sm',
      md: 'h-10 px-4 text-sm'
    }
  },
  defaultVariants: {
    variant: 'primary',
    size: 'md'
  }
});

<button className={button()}>Primary, medium</button>
<button className={button({ size: 'sm' })}>Primary, small</button>
<button className={button({ variant: 'secondary' })}>Secondary, medium</button>
```

Only `size` is overridden on the second button. `variant` stays `primary`. Explicit props win when you need a one-off.

# Extending (https://www.tailwind-variants.org/docs/extending)

Compose component recipes with extend — inherit base styles, variants, slots, and defaults without copying definitions.

`extend` merges one recipe into another. Start from a base button, then add icon-only sizing or new variant tokens without copying the whole definition.

## Basic extend
Pass a parent recipe via the `extend` option:

```ts
import { tv } from 'tailwind-variants';

const baseButton = tv({
  base: 'inline-flex cursor-pointer items-center justify-center rounded-full font-medium select-none transition-colors',
  variants: {
    variant: {
      primary: 'bg-zinc-900 text-white',
      secondary: 'border border-zinc-300 bg-zinc-50 text-zinc-900',
      tertiary: 'text-zinc-700'
    },
    size: {
      sm: 'h-8 px-3 text-sm',
      md: 'h-10 px-4 text-sm'
    }
  },
  defaultVariants: {
    variant: 'primary',
    size: 'md'
  }
});

const iconButton = tv({
  extend: baseButton,
  base: 'gap-2',
  variants: {
    iconOnly: {
      true: 'aspect-square px-0',
      false: ''
    }
  }
});
```

## Merge behavior
`extend` deep-merges:

* **base** — concatenated
* **slots** — merged by key
* **variants** — merged by axis; new keys add axes, existing keys add values
* **defaultVariants** — child overrides parent for matching keys
* **compoundVariants** — arrays concatenated
* **compoundSlots** — arrays concatenated

The returned object exposes merged metadata: `variants`, `variantKeys`, `defaultVariants`, and slot keys reflect the full composed recipe.

## Extending slotted recipes
Slots merge by key. Child recipes can add slots or override styles on inherited ones:

```ts
import { tv } from 'tailwind-variants';

const card = tv({
  slots: {
    base: 'rounded-xl border p-4',
    title: 'text-sm font-medium',
    description: 'text-sm text-zinc-500'
  }
});

const mediaCard = tv({
  extend: card,
  slots: {
    media: 'mb-3 aspect-video rounded-lg bg-zinc-100',
    title: 'text-base font-semibold'
  }
});

const { base, media, title, description } = mediaCard();
```

`mediaCard` keeps `base` / `description` from `card`, adds `media`, and tightens `title`.

## One parent at a time
You can only `extend` one recipe per definition. To combine multiple bases, call them manually or chain extends:

```ts
const extended = tv({ extend: baseButton });
const final = tv({ extend: extended /* ... */ });
```

# FAQ (https://www.tailwind-variants.org/docs/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 bundle size matters and you do not need merge (\~80% smaller).

```ts
import { tv } from 'tailwind-variants';       // merge on
import { tv } from 'tailwind-variants/lite';  // merge off
```

## Do I need tailwind-merge?
**No** — not for Tailwind Variants on v3.3+. Merge is built into the default build.

Keep `tailwind-merge` only if your app imports it directly (`twMerge`, `extendTailwindMerge`, etc.).

## Why `slots: {}`?
An explicit empty `slots` object enables **slot mode** with a single implicit `base` slot:

```ts
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 conflicts      | Returns | Build          |
| --------- | -------------------- | ------- | -------------- |
| `cx`      | No                   | String  | Default + lite |
| `cn`      | Yes (default config) | String  | Default only   |
| `cnMerge` | Yes (custom config)  | Curried | Default only   |

```ts
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` in lite or when classes cannot conflict. Use `cnMerge` when you need per-call config.

## Framework support?
Tailwind Variants is framework-agnostic. It returns class strings — use with any framework or vanilla JS. See [Quick Start](https://www.tailwind-variants.org/docs/quick-start#framework-agnostic) for binding examples.

## Can I extend multiple components?
One `extend` parent per recipe. Chain extends or compose class strings manually for multiple bases.

## TypeScript not inferring variants?
Define variants inline inside `tv`, or use `as const` on external objects:

```ts
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](https://discord.gg/9b6yyZKmH4) or open a [GitHub Discussion](https://github.com/heroui-inc/tailwind-variants/discussions).

# Introduction (https://www.tailwind-variants.org/docs/introduction)

Learn what Tailwind Variants is, why it exists, and the core features for building typed Tailwind component recipes.

**Tailwind Variants** is a first-class variant API for [Tailwind CSS](https://tailwindcss.com/). You define a component recipe once — base styles, variants, defaults, and compound rules — and call it anywhere with typed props.

Design systems need consistency without copy-pasting class strings. Tailwind Variants gives you Stitches-style variants on top of Tailwind: composable, predictable, and framework-agnostic.

## Why Tailwind Variants
* **One recipe, many call sites** — Change `color` or `size` at the call site instead of editing long class strings.
* **Conflict resolution built in** — The default build merges conflicting Tailwind classes. No extra `tailwind-merge` install for TV.
* **Slots for multi-part UI** — Buttons with icons, alerts with titles, cards with headers — each part gets its own styles and variants.
* **TypeScript-first** — Infer variant props with `VariantProps`. Slotted components get typed slot functions.
* **Two builds** — Default (with merge) or `tailwind-variants/lite` when bundle size matters more.

## Key features
### Variants
Define variant keys like `color`, `size`, or boolean flags like `disabled`. Each key maps to Tailwind classes. Think of each key as an independent **axis** when you combine several (for example `color` × `size`).

```ts
import { tv } from 'tailwind-variants';

const button = tv({
  base: 'inline-flex cursor-pointer items-center justify-center rounded-full font-medium select-none transition-colors',
  variants: {
    variant: {
      primary: 'bg-zinc-900 text-white hover:bg-zinc-800',
      secondary: 'border border-zinc-300 bg-zinc-50 text-zinc-900 hover:bg-zinc-100',
      tertiary: 'text-zinc-700 hover:bg-zinc-200/70 hover:text-zinc-950'
    },
    size: {
      sm: 'h-8 px-3 text-sm',
      md: 'h-10 px-4 text-sm'
    }
  },
  defaultVariants: {
    variant: 'primary',
    size: 'md'
  }
});
```

### Slots
Split a component into named parts — `base`, `icon`, `label` — and style each independently.

### Compound variants
Apply extra classes when specific variant combinations match — for example, `color: primary` + `size: lg`.

### Extend
Build on existing recipes. Extend a base button to add an `iconOnly` variant without duplicating styles.

### Responsive
Put Tailwind breakpoint prefixes (`sm:`, `md:`, `lg:`) in your recipe class strings. See [Responsive](https://www.tailwind-variants.org/docs/responsive).

## Community
Issues, feature requests, showcases, and questions are welcome. Pick the channel that fits:

* [Discord](https://discord.gg/9b6yyZKmH4) — chat and quick help
* [GitHub Discussions](https://github.com/heroui-inc/tailwind-variants/discussions) — longer-form questions
* [GitHub Issues](https://github.com/heroui-inc/tailwind-variants/issues) — bugs and proposals
* [Twitter](https://x.com/hero_ui) — announcements and updates

## Credits
Tailwind Variants is heavily inspired by [Stitches](https://stitches.dev/) and [CVA](https://cva.style/).

Thanks to [Tianen Pang](https://github.com/tianenpang) for API design and early library work, [Junior Garcia](https://github.com/jrgarciadev) for building the library and docs, [Mark Skelton](https://github.com/mskelton) for ongoing maintenance, and [Joe Bell](https://github.com/joe-bell) for CVA.

See [Acknowledgements](https://www.tailwind-variants.org/docs/acknowledgements) for the full list of inspirations and dependencies.

# LLMs.txt (https://www.tailwind-variants.org/docs/llms-txt)

Machine-readable Tailwind Variants docs for AI agents — llms.txt index, full corpus dump, per-page Markdown, and Accept-based content negotiation.

Docs are also available as Markdown so tools can read them without scraping HTML. Layout follows [llms.txt](https://llmstxt.org/).

## Endpoints
| URL               | Format   | Purpose                                                                  |
| ----------------- | -------- | ------------------------------------------------------------------------ |
| `/llms.txt`       | Markdown | Curated index: project summary plus links to every docs page’s `.md` URL |
| `/llms-full.txt`  | Markdown | Entire docs set in one file when you need the full corpus in context     |
| `/docs/{slug}.md` | Markdown | Single page as Markdown — stable URL for fetch, copy, and editor tools   |
| `/docs/{slug}`    | HTML     | Normal docs page; same Markdown as `.md` when `Accept` prefers it        |

## Content negotiation
```http
GET /docs/variants HTTP/1.1
Accept: text/markdown
```

Browsers get HTML. Clients that prefer Markdown get the same body as `/docs/variants.md`.

## Per-page Markdown
```
/docs/quick-start.md
/docs/slots.md
/docs/api-reference.md
```

Each response is the page title, URL, and Markdown body (demos expanded to code fences).

## Usage
1. Start with `/llms.txt`.
2. Use `/llms-full.txt` when you need the whole corpus.
3. Use `/docs/{slug}.md` for a single topic.
4. Or call the [MCP server](https://www.tailwind-variants.org/docs/mcp-server) from an editor agent.

# MCP Server (https://www.tailwind-variants.org/docs/mcp-server)

Query Tailwind Variants docs from AI tools via the Model Context Protocol — list pages, full-text search, and fetch Markdown by path.

The docs site runs an MCP server so agents can list, search, and fetch pages programmatically.

## Endpoint
**Streamable HTTP** at:

```
https://www.tailwind-variants.org/api/mcp
```

Works with MCP clients that support Streamable HTTP transport (Cursor, Claude Desktop with remote servers, etc.).

## Tools
| Tool          | Description                                                          |
| ------------- | -------------------------------------------------------------------- |
| `list_pages`  | All doc pages with title, description, HTML/Markdown URLs, and slugs |
| `search_docs` | Full-text search across title, description, URL, and page body       |
| `get_page`    | Full Markdown for a page by path or slug                             |

### Examples
**List pages** — no arguments.

**Search:**

```json
{ "query": "slots" }
```

**Get page** — accepts `/docs/variants`, `variants`, or `slots`:

```json
{ "path": "/docs/slots" }
```

## Cursor configuration
Add to `.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "tailwind-variants": {
      "url": "https://www.tailwind-variants.org/api/mcp"
    }
  }
}
```

For local development against this repo:

```json
{
  "mcpServers": {
    "tailwind-variants": {
      "url": "http://localhost:3000/api/mcp"
    }
  }
}
```

Restart Cursor after saving. The agent can then call `list_pages`, `search_docs`, and `get_page` while you work.

## Related
For raw text dumps without MCP, see [LLMs.txt](https://www.tailwind-variants.org/docs/llms-txt).

# Migration (https://www.tailwind-variants.org/docs/migration)

Upgrade guides for moving between Tailwind Variants major versions, including v2 to v3 and the lite build split.

## To v3.3.0
### Drop `tailwind-merge` for TV
Conflict resolution ships in the default build. Install only Tailwind Variants unless your app calls `tailwind-merge` directly:

```diff
- npm i tailwind-variants tailwind-merge
+ npm i tailwind-variants
```

Keep `tailwind-merge` if you still use `twMerge`, `extendTailwindMerge`, or similar elsewhere.

### Slots empty object
Passing `slots: {}` enables slot mode with an implicit `base` slot. Omit `slots` when you want a plain string return.

```ts
// Slot mode — returns { base: () => string }
const x = tv({ slots: {}, base: 'p-4' });

// String mode — omit slots
const y = tv({ base: 'p-4' });
```

### `cn` → `cnMerge` for config
The v3.2.2 split still applies:

```ts
// Before
cn('px-2', 'px-4')({ twMerge: false });

// After
cnMerge('px-2', 'px-4')({ twMerge: false });
cn('px-2', 'px-4'); // string, default merge
```

### Extended types
Extended component metadata now reflects merged variants, slots, and keys at runtime. Slotted components include the implicit `base` slot in types.

***

## v3.2.0 → v3.2.2
### `cn` returns a string
`cn` no longer accepts a config callback. Use `cnMerge` for custom merge behavior.

```ts
import { cn, cnMerge } from 'tailwind-variants';

cn('px-2', 'px-4'); // => "px-4"
cnMerge('px-2', 'px-4')({ twMerge: false }); // => "px-2 px-4"
```

***

## v3.1.1 → v3.2.0
### Replace `cnBase` with `cx`
```diff
- import { cnBase } from 'tailwind-variants';
+ import { cx } from 'tailwind-variants';

- cnBase('flex gap-2', className);
+ cx('flex gap-2', className);
```

### `cn` defaults to merge
On the default build, `cn` resolves conflicting Tailwind classes automatically.

***

## v2 → v3
### Two builds
```ts
// Default — conflict resolution included (v3.3+)
import { tv, cn, cx } from 'tailwind-variants';

// Lite — no merge, ~80% smaller
import { tv, cx } from 'tailwind-variants/lite';
```

### Removed APIs
* `responsiveVariants` — use Tailwind responsive prefixes in class strings ([Responsive](https://www.tailwind-variants.org/docs/responsive))
* `withTv` — use `tv` directly
* Lazy merge loading — replaced by explicit default vs lite builds

### Performance
v3 is significantly faster for merge operations. The default build includes merge; lite skips it entirely.

See [Class Resolution](https://www.tailwind-variants.org/docs/class-resolution) and [Releases](https://www.tailwind-variants.org/docs/releases) for v3.3.0 details.

# Overrides (https://www.tailwind-variants.org/docs/overrides)

Pass one-off class overrides at the call site with class or className without changing the shared recipe.

Pass extra classes when calling a recipe to customize a single instance — for example a full-width button or tighter spacing.

## Global override with `class` or `className`
Both props work identically. Pass them when calling the recipe:

```ts
import { tv } from 'tailwind-variants';

const button = tv({
  base: 'inline-flex cursor-pointer items-center justify-center rounded-full px-4 py-2 text-sm font-medium select-none',
  variants: {
    variant: {
      primary: 'bg-zinc-900 text-white'
    }
  }
});

button({ variant: 'primary' });
button({ variant: 'primary', class: 'w-full' });
```

Overrides merge with variant output. In the default build, conflicting Tailwind utilities resolve — later wins according to merge rules.

## Slot overrides
With slots, pass overrides to individual slot functions:

```ts
const alert = tv({
  slots: {
    base: 'flex gap-3 rounded-lg p-4',
    title: 'font-semibold'
  }
});

const { base, title } = alert();

base({ class: 'border border-zinc-200' });
title({ className: 'text-lg' });
```

Each slot function accepts the same override props as the root call.

## Slotted root overrides
You can also pass overrides to the root call — they apply to the implicit or explicit `base` slot:

```ts
const slots = alert({ class: 'max-w-md' });
slots.base(); // includes max-w-md
```

# Quick Start (https://www.tailwind-variants.org/docs/quick-start)

Install Tailwind Variants, choose default or lite, create your first tv recipe, and set up editor IntelliSense.

## Install
```bash
npm install tailwind-variants
```

Conflict resolution is included in the default build. You do not need `tailwind-merge` unless your app calls it directly elsewhere.

For the lite build (no merge, smaller bundle):

```ts
import { tv } from "tailwind-variants/lite";
```

## Your first component
Define a recipe with `tv`, then call it like a function. Defaults keep call sites short; pass props only when you need to diverge.

```tsx
import { tv } from 'tailwind-variants';

const button = tv({
  base: 'inline-flex cursor-pointer items-center justify-center rounded-full font-medium select-none transition-colors',
  variants: {
    variant: {
      primary: 'bg-zinc-900 text-white hover:bg-zinc-800',
      secondary: 'border border-zinc-300 bg-zinc-50 text-zinc-900 hover:bg-zinc-100',
      tertiary: 'text-zinc-700 hover:bg-zinc-200/70 hover:text-zinc-950'
    },
    size: {
      sm: 'h-8 px-3 text-sm',
      md: 'h-10 px-4 text-sm'
    }
  },
  defaultVariants: {
    variant: 'primary',
    size: 'md'
  }
});

<button className={button()}>Default button</button>
<button className={button({ variant: 'secondary', size: 'sm' })}>
  Click me
</button>
<button className={button({ class: 'w-full' })}>Full width</button>
```

## Editor setup
### VS Code IntelliSense
So Tailwind CSS IntelliSense can complete classes inside `tv` strings, add this to your VS Code settings:

```json
{
  "tailwindCSS.experimental.classRegex": [
    ["tv\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
  ]
}
```

### Prettier
If you use [`prettier-plugin-tailwindcss`](https://github.com/tailwindlabs/prettier-plugin-tailwindcss), include `tv` in the function list so class order stays consistent:

```js
// prettier.config.js
module.exports = {
  plugins: ["prettier-plugin-tailwindcss"],
  tailwindFunctions: ["tv", "cn", "cx"],
};
```

## Framework agnostic
Tailwind Variants is framework-agnostic. Use the class string with any framework or vanilla JS:

```ts
import { tv } from "tailwind-variants";

const button = tv({
  base: "inline-flex cursor-pointer items-center justify-center rounded-md px-4 py-2 text-sm font-medium select-none",
  variants: {
    color: {
      primary: "bg-blue-600 text-white",
      secondary: "bg-zinc-200 text-zinc-900",
    },
  },
  defaultVariants: {
    color: "primary",
  },
});
```

**React**

```tsx
<button className={button({ color: "primary" })}>Save</button>
```

**Vue**

```vue
<button :class="button({ color: 'primary' })">Save</button>
```

**Svelte**

```svelte
<button class={button({ color: 'primary' })}>Save</button>
```

**Solid**

```tsx
<button class={button({ color: "primary" })}>Save</button>
```

**Angular**

```html
<button [class]="button({ color: 'primary' })">Save</button>
```

**Vanilla**

```ts
element.className = button({ color: "primary" });
```

# Recipes (https://www.tailwind-variants.org/docs/recipes)

Copy-paste Tailwind Variants recipes for common UI patterns like buttons, alerts, and badges.

## Button
```ts
import { tv, type VariantProps } from 'tailwind-variants';

export const button = tv({
  base: [
    'inline-flex cursor-pointer items-center justify-center gap-2 select-none',
    'rounded-lg font-medium transition-colors',
    'disabled:cursor-not-allowed disabled:opacity-50'
  ],
  variants: {
    variant: {
      primary: 'bg-zinc-900 text-white hover:bg-zinc-800',
      secondary: 'border border-zinc-300 bg-zinc-50 text-zinc-900 hover:bg-zinc-100',
      tertiary: 'text-zinc-700 hover:bg-zinc-200/70 hover:text-zinc-950'
    },
    size: {
      sm: 'h-8 px-3 text-sm',
      md: 'h-10 px-4 text-sm',
      lg: 'h-12 px-6 text-base'
    }
  },
  compoundVariants: [
    { variant: 'primary', size: 'lg', class: 'shadow-lg shadow-zinc-900/15' },
    { variant: 'secondary', size: 'lg', class: 'shadow-sm' }
  ],
  defaultVariants: {
    variant: 'primary',
    size: 'md'
  }
});

export type ButtonVariants = VariantProps<typeof button>;
```

## Alert (with slots)
```ts
import { tv, type VariantProps } from 'tailwind-variants';

export const alert = tv({
  slots: {
    base: 'flex gap-3 rounded-lg p-4 border',
    icon: 'size-5 shrink-0 mt-0.5',
    content: 'flex-1 min-w-0',
    title: 'font-semibold leading-snug',
    description: 'text-sm mt-1 opacity-90'
  },
  variants: {
    color: {
      default: {
        base: 'bg-zinc-50 border-zinc-200 text-zinc-900',
        icon: 'text-zinc-500'
      },
      success: {
        base: 'bg-green-50 border-green-200 text-green-900',
        icon: 'text-green-600'
      },
      warning: {
        base: 'bg-amber-50 border-amber-200 text-amber-900',
        icon: 'text-amber-600'
      },
      danger: {
        base: 'bg-red-50 border-red-200 text-red-900',
        icon: 'text-red-600'
      }
    }
  },
  defaultVariants: {
    color: 'default'
  }
});

export type AlertVariants = VariantProps<typeof alert>;
```

## Badge
```ts
import { tv, type VariantProps } from 'tailwind-variants';

export const badge = tv({
  base: 'inline-flex select-none items-center rounded-full font-medium',
  variants: {
    variant: {
      solid: '',
      soft: '',
      outline: 'border bg-transparent'
    },
    color: {
      default: '',
      primary: '',
      success: '',
      danger: ''
    },
    size: {
      sm: 'px-2 py-0.5 text-xs',
      md: 'px-2.5 py-0.5 text-sm'
    }
  },
  compoundVariants: [
    { variant: 'solid', color: 'default', class: 'bg-zinc-900 text-white' },
    { variant: 'solid', color: 'primary', class: 'bg-blue-600 text-white' },
    { variant: 'solid', color: 'success', class: 'bg-green-600 text-white' },
    { variant: 'solid', color: 'danger', class: 'bg-red-600 text-white' },
    { variant: 'soft', color: 'default', class: 'bg-zinc-100 text-zinc-700' },
    { variant: 'soft', color: 'primary', class: 'bg-blue-100 text-blue-700' },
    { variant: 'outline', color: 'default', class: 'border-zinc-300 text-zinc-700' },
    { variant: 'outline', color: 'primary', class: 'border-blue-300 text-blue-700' }
  ],
  defaultVariants: {
    variant: 'soft',
    color: 'default',
    size: 'sm'
  }
});

export type BadgeVariants = VariantProps<typeof badge>;
```

# Releases (https://www.tailwind-variants.org/docs/releases)

Notable Tailwind Variants releases and changes, including performance, merge, and API updates.

## v3.3.1
Patch release fixing slots shared-state contamination ([#305](https://github.com/heroui-inc/tailwind-variants/issues/305)).

## v3.3.0
Stronger TypeScript inference, built-in conflict resolution, and less setup than earlier v3 releases.

### Highlights
* **Fully rewritten in TypeScript** — stronger inference for variants, slots, and `extend`
* **Conflict resolution included** — no `tailwind-merge` install required for TV
* **Performance improvements** — faster recipe resolution and class merging
* **Optional `tailwindcss` peer** — use TV outside Tailwind-only setups more easily

### Conflict resolution
The default `tailwind-variants` entry includes merge logic inspired by [tailwind-merge](https://github.com/dcastil/tailwind-merge). Install one package:

```bash
npm install tailwind-variants
```

* Existing `twMergeConfig` objects generally keep working — prefer `{ extend, override }`
* Lite (`tailwind-variants/lite`) still has no merge
* Utilities: `tv`, `createTV`, `cn`, `cnMerge`, `cx`

### Behavior changes
* **`slots: {}`** — explicit empty object enables slot mode with implicit `base`; omit `slots` for string return
* **Extended metadata types** — describe merged variants, slots, and keys at runtime
* **`cn(...)(config)`** — use `cnMerge` instead (see [Migration](https://www.tailwind-variants.org/docs/migration))

### Upgrade steps
1. Remove `tailwind-merge` if it was only installed for TV
2. Keep `twMergeConfig` as-is; use `extend` / `override` for custom tokens
3. Switch to `/lite` only when you intentionally skip merge

```diff
- npm i tailwind-variants tailwind-merge
+ npm i tailwind-variants
```

### Acknowledgements
Conflict resolution draws on MIT-licensed work from [tailwind-merge](https://github.com/dcastil/tailwind-merge), [clsx](https://github.com/lukeed/clsx), and [cnfast](https://github.com/aidenybai/cnfast). See [Acknowledgements](https://www.tailwind-variants.org/docs/acknowledgements).

***

## Earlier v3 releases
### v3.2.2
* `cn` returns a string directly; `cnMerge` added for custom config
* Better TypeScript support without Proxy types

### v3.2.0
* `cx` replaces `cnBase` for lightweight concat
* `cn` defaults to conflict resolution on the default build

### v3.0.0
* Default vs lite build split
* Major performance improvements
* Framework-agnostic API retained

See [Migration](https://www.tailwind-variants.org/docs/migration) for step-by-step upgrade paths.

# Responsive (https://www.tailwind-variants.org/docs/responsive)

Use Tailwind breakpoint prefixes like sm: and md: inside base, variant, and slot class strings — no TV-specific API.

Responsive styles are plain Tailwind. Put breakpoint prefixes like `sm:`, `md:`, and `lg:` directly in your `base`, variant, and slot class strings — there is no TV-specific responsive API.

See [Tailwind responsive design](https://tailwindcss.com/docs/responsive-design) for breakpoint syntax.

> **info:** The old `responsiveVariants` option was removed. It depended on Tailwind's `content.transform`, which [Tailwind CSS v4](https://www.tailwind-variants.org/docs/tailwind-v4) no longer supports. Prefixes in class strings replace it.

## In `base`
Scale shared styles across breakpoints. Drag the preview — media and type step up at `sm` / `md`, and from `sm` the title block and price sit on one row:

```ts
import { tv } from 'tailwind-variants';

const stay = tv({
  base: 'rounded-xl border p-3.5 sm:p-4 md:p-5'
});

const media = tv({
  base: 'flex h-28 items-center justify-center rounded-lg bg-zinc-100 sm:h-36 md:h-40'
});

const details = tv({
  base: [
    'mt-3 flex flex-col gap-2.5',
    'sm:mt-3.5 sm:flex-row sm:items-end sm:justify-between'
  ]
});

const place = tv({
  base: 'text-[0.9375rem] font-semibold tracking-tight sm:text-lg md:text-xl'
});

const price = tv({
  base: 'text-[0.8125rem] sm:shrink-0 sm:text-right sm:text-sm'
});
```

## In variants
Bake responsive utilities into a variant value. Here `size` is a **variant name**; the `sm:` prefixes inside those strings are the responsive part — the primary action stays full-width until `sm`:

```ts
import { tv } from 'tailwind-variants';

const button = tv({
  base: 'inline-flex cursor-pointer items-center justify-center rounded-lg font-medium select-none transition-colors',
  variants: {
    variant: {
      primary: 'bg-zinc-900 text-white',
      secondary: 'border border-zinc-300 bg-zinc-50 text-zinc-900',
      tertiary: 'text-zinc-700 hover:bg-zinc-200/70'
    },
    size: {
      sm: 'h-9 px-3 text-[0.8125rem] sm:px-3.5 sm:text-sm',
      lg: 'h-10 w-full px-4 text-sm sm:w-auto sm:min-w-36'
    }
  },
  defaultVariants: {
    variant: 'primary',
    size: 'sm'
  }
});

button({ size: 'lg' });
// full-width CTA → inline from sm
```

## With slots
A calendar event: from `sm` title and place share one line; from `md` the card goes horizontal and those two stack again:

```ts
import { tv } from 'tailwind-variants';

const event = tv({
  slots: {
    root: [
      'flex flex-col gap-3.5 rounded-xl border p-3.5 sm:p-4',
      'md:flex-row md:items-center md:gap-4'
    ],
    time: [
      'flex w-fit flex-col self-start rounded-lg bg-zinc-100 px-3 py-2.5',
      'md:min-w-[4.5rem] md:items-center md:self-auto md:px-2.5 md:py-3'
    ],
    hour: 'text-sm font-semibold tracking-tight sm:text-[0.9375rem]',
    day: 'mt-0.5 text-[0.6875rem] text-zinc-500',
    body: [
      'flex min-w-0 flex-1 flex-col gap-0.5',
      'sm:flex-row sm:items-baseline sm:gap-2',
      'md:flex-col md:items-stretch md:gap-0.5'
    ],
    title: 'shrink-0 text-[0.875rem] font-medium sm:text-[0.9375rem]',
    place: [
      'min-w-0 text-[0.75rem] text-zinc-500',
      'sm:truncate sm:text-[0.8125rem]',
      'md:overflow-visible md:text-clip'
    ],
    actions: 'flex w-full gap-2 md:w-auto md:shrink-0'
  }
});

const { root, time, hour, day, body, title, place, actions } = event();
```

## At the call site
Overrides accept responsive classes too:

```ts
button({
  variant: 'primary',
  class: 'w-full sm:w-auto'
});
```

## Conflict resolution
Responsive utilities are distinct classes (`text-sm` vs `sm:text-base`). The default build merges conflicts **within the same breakpoint** — for example `sm:px-2` vs `sm:px-4` — the same way it merges unprefixed utilities. See [Class resolution](https://www.tailwind-variants.org/docs/class-resolution).

# Slots (https://www.tailwind-variants.org/docs/slots)

Style multi-part components with named slots, per-slot variants, and compound slots from a single recipe.

Slots let you style and variant each part of a multi-part component — icon, label, wrapper, content — from one recipe.

## Enable slot mode
Pass a `slots` object. Each key becomes a named slot function on the return value:

```ts
import { tv } from 'tailwind-variants';

const alert = tv({
  slots: {
    base: 'flex gap-3 rounded-lg p-4',
    icon: 'size-5 shrink-0',
    title: 'font-semibold',
    description: 'text-sm opacity-80'
  }
});

const { base, icon, title, description } = alert();

base();
title();
```

> **info:** Pass an explicit empty `slots: {}` to enable slot mode with an implicit `base` slot only. Omit `slots` entirely when you want a plain class string return.

## Variants per slot
With slots, variant values must be **objects** — one class string (or array) per slot:

```ts
import { tv } from 'tailwind-variants';

const alert = tv({
  slots: {
    base: 'flex gap-3 rounded-lg p-4',
    icon: 'size-5 shrink-0',
    title: 'font-semibold',
    description: 'text-sm opacity-80'
  },
  variants: {
    color: {
      default: {
        base: 'bg-zinc-100 text-zinc-900',
        icon: 'text-zinc-500',
        title: 'text-zinc-900'
      },
      danger: {
        base: 'bg-red-50 text-red-900',
        icon: 'text-red-500',
        title: 'text-red-900'
      }
    }
  },
  defaultVariants: {
    color: 'default'
  }
});

const slots = alert({ color: 'danger' });
slots.base();
slots.icon();
```

TV selects the correct classes for each slot automatically.

## Compound slots
`compoundSlots` apply extra classes to specific slots when variant conditions match — same idea as compound variants, but targeted per slot:

```ts
const card = tv({
  slots: { base: 'rounded-xl p-4', header: 'font-bold' },
  variants: {
    elevated: { true: {}, false: {} }
  },
  compoundSlots: [
    {
      elevated: true,
      slots: ['base'],
      class: 'shadow-lg'
    }
  ]
});
```

Use compound slots when a combination should restyle only certain parts.

# Tailwind CSS v4 (https://www.tailwind-variants.org/docs/tailwind-v4)

Use Tailwind Variants with Tailwind CSS v4 — content scanning, conflict resolution, and what stays the same in recipes.

Tailwind Variants works with Tailwind CSS v4. Class strings are plain Tailwind utilities — TV does not depend on a specific Tailwind major version.

## No TV-specific migration
Upgrade Tailwind and TV independently:

```bash
npm install tailwindcss@latest tailwind-variants@latest
```

Your recipes, variants, and slots stay the same. Conflict resolution in the default build handles v4 utility names.

## Content sources
Tailwind v4 scans sources via `@source` in CSS instead of `content` in a JS config. Make sure files that **call** `tv()` are included so Tailwind generates the classes your variants reference:

```css
@import "tailwindcss";

@source "../components/**/*.{js,ts,jsx,tsx}";
@source "../node_modules/your-ui-lib/dist/**/*.{js,ts,jsx,tsx}";
```

If a class only appears inside a TV recipe string, the file defining that recipe must be in a scanned path.

## Responsive variants
Tailwind v4 removed `config.content.transform`, so TV's old `responsiveVariants` option is gone. Use Tailwind responsive prefixes directly in your classes — see [Responsive](https://www.tailwind-variants.org/docs/responsive):

```ts
const text = tv({
  base: 'text-sm md:text-base lg:text-lg',
  variants: {
    emphasis: {
      high: 'font-bold md:font-extrabold'
    }
  }
});
```

## Removed APIs
These were removed in earlier major versions and do not apply to v3:

* `responsiveVariants` — use responsive prefixes in class strings
* `withTv` — use `tv` directly

# TypeScript (https://www.tailwind-variants.org/docs/typescript)

Extract typed props with VariantProps, work with slotted return types, and keep variant inference reliable.

Tailwind Variants is written in TypeScript. Variant keys and values infer automatically — no code generation required.

## `VariantProps`
Extract the props a recipe accepts:

```tsx
import { tv, type VariantProps } from 'tailwind-variants';

export const button = tv({
  base: 'inline-flex cursor-pointer items-center justify-center rounded-full px-4 py-1.5 font-medium select-none',
  variants: {
    variant: {
      primary: 'bg-zinc-900 text-white',
      secondary: 'bg-zinc-100 text-zinc-900',
      tertiary: 'text-zinc-600'
    },
    flat: {
      true: 'bg-transparent shadow-none'
    }
  },
  defaultVariants: {
    variant: 'primary'
  }
});

type ButtonVariants = VariantProps<typeof button>;
// variant?: "primary" | "secondary" | "tertiary"
// flat?: boolean

interface ButtonProps extends ButtonVariants {
  children: React.ReactNode;
  className?: string;
}

export function Button({ children, className, ...variants }: ButtonProps) {
  return (
    <button className={button({ ...variants, className })}>
      {children}
    </button>
  );
}
```

Keys with `defaultVariants` become optional on the type.

## Required variants
TV does not have a built-in "required variant" flag. Use TypeScript utilities:

```ts
type ButtonVariants = VariantProps<typeof button>;

type RequiredSize = Omit<ButtonVariants, 'size'> &
  Required<Pick<ButtonVariants, 'size'>>;
```

Or model the axis without a default so TypeScript keeps it required.

## Slotted return types
Slotted recipes return an object of slot functions. Destructure once for cleaner types:

```ts
const alert = tv({
  slots: {
    base: 'flex gap-3 rounded-lg p-4',
    title: 'font-semibold',
    description: 'text-sm'
  },
  variants: {
    color: {
      default: {
        base: 'bg-zinc-100',
        title: 'text-zinc-900'
      },
      danger: {
        base: 'bg-red-50',
        title: 'text-red-900'
      }
    }
  }
});

type AlertSlots = ReturnType<typeof alert>;
// AlertSlots.base, .title, .description — each (props?) => string

function Alert({ color }: VariantProps<typeof alert>) {
  const { base, title, description } = alert({ color });

  return (
    <div className={base()}>
      <p className={title()}>Title</p>
      <p className={description()}>Body</p>
    </div>
  );
}
```

## `as const` for external definitions
When variants live outside `tv`, use `as const` so TypeScript preserves literal keys:

```ts
const variants = {
  primary: 'bg-zinc-900 text-white',
  secondary: 'bg-zinc-100 text-zinc-900',
  tertiary: 'text-zinc-600'
} as const;

const button = tv({
  variants: { variant: variants }
});
```

# Variants (https://www.tailwind-variants.org/docs/variants)

Define base styles and variant keys like color, size, and disabled on a tv recipe for typed call-site props.

Variants turn one component definition into many visual states. You set a **base** — shared classes every call site gets — then add keys like `variant`, `size`, or `disabled`.

## Base + variants
```ts
import { tv } from 'tailwind-variants';

const button = tv({
  base: 'inline-flex cursor-pointer items-center justify-center rounded-full px-4 py-2 text-sm font-medium select-none transition-colors',
  variants: {
    variant: {
      primary: 'bg-zinc-900 text-white hover:bg-zinc-800',
      secondary: 'border border-zinc-300 bg-zinc-50 text-zinc-900 hover:bg-zinc-100',
      tertiary: 'text-zinc-700 hover:bg-zinc-200/70 hover:text-zinc-950'
    }
  }
});

button({ variant: 'secondary' });
```

Base classes apply first. Variant classes layer on top. Conflicting utilities resolve automatically in the default build.

## Multiple variants
Combine as many variant keys as you need. Each key is an independent axis (`color` × `size` × `disabled`):

```ts
const button = tv({
  base: 'inline-flex cursor-pointer items-center justify-center rounded-full font-medium select-none transition-colors',
  variants: {
    variant: {
      primary: 'bg-zinc-900 text-white',
      secondary: 'border border-zinc-300 bg-zinc-50 text-zinc-900',
      tertiary: 'text-zinc-700'
    },
    size: {
      sm: 'h-8 px-3 text-sm',
      md: 'h-10 px-4 text-sm',
      lg: 'h-11 px-5 text-base'
    }
  }
});

button({ variant: 'primary', size: 'lg' });
```

## Boolean variants
Use `true` / `false` keys for state flags:

```ts
const button = tv({
  base: 'inline-flex cursor-pointer items-center justify-center rounded-full bg-zinc-900 px-4 py-2 text-sm font-medium text-white select-none',
  variants: {
    disabled: {
      true: 'cursor-not-allowed opacity-45',
      false: ''
    }
  }
});

button({ disabled: true });
```

Boolean variants work well for `disabled`, `active`, `loading`, or feature toggles.

## Array values
Variant values accept arrays. TV flattens them like `clsx`:

```ts
const badge = tv({
  base: 'inline-flex select-none items-center rounded-full border px-2.5 py-0.5 text-sm font-medium',
  variants: {
    color: {
      primary: ['bg-zinc-100', 'text-zinc-800', 'border-zinc-300']
    }
  }
});
```
