Skip to content

Installation

This content is for Beta. Switch to the latest version for up-to-date documentation.

Terminal window
pnpm i cva@beta
import { cx } from "cva";
const active = true;
cx("button", ["rounded", { "button--active": active }]);
// => "button rounded button--active"

cx joins class names; it doesn’t resolve conflicting CSS rules. See the cx reference for its inputs and handling class conflicts for Tailwind CSS options.

import { cva } from "cva";
const badge = cva({
variants: {
tone: {
info: "bg-blue-500 text-white",
warning: "bg-amber-500 text-black",
},
},
});

See Tailwind’s class detection guide for how source scanning works. The editor settings below add autocompletion inside cva.

You can enable autocompletion inside cva using the steps below:

  1. Install the “Tailwind CSS IntelliSense” Visual Studio Code extension

  2. Add the following to your .vscode/settings.json:

    .vscode/settings.json
    {
    "tailwindCSS.classFunctions": ["cva", "cx"],
    }

By default, cva joins classes with clsx. It does not resolve conflicting Tailwind CSS utilities. Choose one of the approaches below:

Install the cn package to join conditional classes and resolve Tailwind CSS conflicts in one function.

The shadcn CLI installs cva@beta with cn and writes cva.config.ts to your project root:

Terminal window
pnpm dlx shadcn@latest add joe-bell/cva/cn

To set it up by hand instead, add cn and export the configured functions from cva.config.ts:

Terminal window
pnpm add cn
cva.config.ts
import { defineConfig } from "cva/config";
import { cn as merge } from "cn";
export const { cva, cx: cn } = defineConfig({ cx: merge });

cva/tailwindcss is a set of custom Tailwind CSS variants, for use with or without cva. Import it after Tailwind CSS:

@import "tailwindcss";
@import "cva/tailwindcss";

The base: variant marks a utility as a component default, so any ordinary utility overrides it through the CSS cascade instead of runtime class merging.

Prefix overridable component defaults with base:, including defaults selected by variants or compound variants. Write state styles as ordinary utilities:

import { cva } from "cva";
export const button = cva({
base: [
"base:bg-blue-600 base:px-4 base:py-2 base:text-white",
"hover:bg-blue-700",
],
});
button({ className: "bg-violet-600" });

In this example, bg-violet-600 wins over base:bg-blue-600, and hover:bg-blue-700 still applies on hover.

base: utilities sit in a nested layer below ordinary utilities but above Tailwind’s base and components layers. Inspired by Diego Haz’s base: idea and a related :where() example.

base: makes defaults overridable. It does not merge classes, so keep these rules in mind:

  • Conditional defaults lose too: any ordinary bg-* utility beats base:hover:bg-*, base:sm:bg-*, and base:dark:bg-*, even while the condition matches. Write state styles without base:.
  • Class order still doesn’t matter: two ordinary utilities that conflict, or two base: utilities that conflict, resolve by stylesheet order.
  • Put base: before pseudo-element variants: base:before:bg-* works, but before:base:bg-* compiles to an invalid selector and never applies.
  • !important flips the layer order: base:bg-blue-600! beats bg-violet-600!.

For runtime conflict resolution, cn and tailwind-merge remove earlier conflicting utilities from the class string.

Install tailwind-merge and combine it with the cx already exported by cva. This preserves strings, arrays, and conditional objects while resolving Tailwind CSS conflicts:

Terminal window
pnpm add tailwind-merge
cva.config.ts
import { defineConfig } from "cva/config";
import { cx as joinClasses } from "cva";
import { twMerge } from "tailwind-merge";
export const { cva, cx: cn } = defineConfig({
cx: (...inputs) => twMerge(joinClasses(...inputs)),
});

Bare twMerge

Use bare twMerge when your authored values are strings or arrays. TypeScript rejects object syntax, but JavaScript has no compile-time protection. Exported configurations can expose tailwind-merge’s ClassNameValue in your library declarations.

import { defineConfig } from "cva/config";
import { twMerge } from "tailwind-merge";
export const { cva, cx: cn } = defineConfig({ cx: twMerge });

Use the configured functions throughout your project:

components/button.ts
import { cn, cva } from "../cva.config";
export const button = cva({
base: "font-semibold bg-gray-200 border rounded",
variants: {
intent: {
primary: "bg-blue-500 text-white border-transparent hover:bg-blue-600",
secondary: "bg-white text-gray-800 border-gray-400 hover:bg-gray-100",
},
},
defaultVariants: {
intent: "primary",
},
});

Both configurations produce the same output:

button();
// => "font-semibold border rounded bg-blue-500 text-white border-transparent hover:bg-blue-600"
cn("bg-gray-200", { "bg-blue-500": true });
// => "bg-blue-500"

For string-only authoring without conflict resolution, see advanced concatenators for clsx/lite.