Installation
This content is for Beta. Switch to the latest version for up-to-date documentation.
pnpm i cva@betanpm i cva@betayarn add cva@betabun add cva@betadeno add cva@betaTailwind CSS
Section titled “Tailwind CSS”If you’re a Tailwind user, here are some additional (optional) steps to get the most out of cva:
IntelliSense
Section titled “IntelliSense”You can enable autocompletion inside cva using the steps below:
-
Install the “Tailwind CSS IntelliSense” Visual Studio Code extension
-
Add the following to your
.vscode/settings.json:.vscode/settings.json {"tailwindCSS.classFunctions": ["cva", "cx"],}
Add the following to your .zed/settings.json:
{ "lsp": { "tailwindcss-language-server": { "settings": { "classFunctions": ["cva", "cx"], } } }}-
Add the following configuration:
require 'lspconfig'.tailwindcss.setup({settings = {tailwindCSS = {classFunctions = { "cva", "cx" },},},})
-
Check the version. Available for WebStorm 2023.1 and later
-
Open the settings. Go to Languages and Frameworks | Style Sheets | Tailwind CSS
-
Add the following to your tailwind configuration
{"classFunctions": ["cva", "cx"]}
Handling style conflicts
Section titled “Handling style conflicts”If you want to merge Tailwind CSS classes without conflicts, swap cva’s concatenator via defineConfig from cva/core: pass tailwind-merge, a drop-in like cnfast, or your own function as the cx option.
Your concatenator owns the class name grammar entirely: cva assembles the authored values (base, matched variants, class/className) and passes them through verbatim, one argument each. The authoring surface adopts your concatenator’s input type automatically, so your variants are checked against exactly what it supports.
Example with tailwind-merge
import { defineConfig } from "cva/core";import { twMerge } from "tailwind-merge";
export const { cva, cx } = defineConfig({ cx: twMerge,});twMerge accepts strings and arrays but not objects, so this cva’s variants are typechecked against tailwind-merge’s own input type: object syntax becomes a compile error instead of a silently dropped class. Prefer to keep clsx’s object syntax available? Compose the two instead: cx: (...inputs) => twMerge(clsx(inputs)).
import { cx, cva } from "../cva.config";
export const button = cva({ // 1. `twMerge` strips out `bg-gray-200`… base: "font-semibold bg-gray-200 border rounded", variants: { intent: { // 2. …as variant `bg-*` values take precedence 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", },});
button();// => "font-semibold border rounded bg-blue-500 text-white border-transparent hover:bg-blue-600"
cx("bg-gray-200", "bg-blue-500");// => "bg-blue-500"Example with cnfast
cnfast’s cn combines clsx and tailwind-merge behavior in one function, and its input type matches cva’s default contract, so it drops straight in with objects and arrays intact.
import { defineConfig } from "cva/core";import { cn } from "cnfast";
export const { cva, cx } = defineConfig({ cx: cn,});
cx("bg-gray-200", { "bg-blue-500": true });// => "bg-blue-500"Example with clsx/lite
If your components are authored with plain strings only and you don’t need conflict resolution, clsx/lite is the smallest option. Heads up: its runtime silently ignores anything that isn’t a string (arrays, objects, numbers), even though its published types claim otherwise, so keep your variant values as strings.
import { defineConfig } from "cva/core";import { clsx } from "clsx/lite";
export const { cva, cx } = defineConfig({ cx: clsx,});
cx("foo", "bar", "baz");// => "foo bar baz"