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@betaJoin conditional classes
Section titled “Join conditional classes”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.
Tailwind CSS
Section titled “Tailwind CSS”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.
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 class conflicts
Section titled “Handling class conflicts”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:
pnpm dlx shadcn@latest add joe-bell/cva/cnTo set it up by hand instead, add cn and export the configured functions from cva.config.ts:
pnpm add cnimport { defineConfig } from "cva/config";import { cn as merge } from "cn";
export const { cva, cx: cn } = defineConfig({ cx: merge });cva/tailwindcss
Section titled “cva/tailwindcss”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.
Limitations
Section titled “Limitations”base: makes defaults overridable. It does not merge classes, so keep these rules in mind:
- Conditional defaults lose too: any ordinary
bg-*utility beatsbase:hover:bg-*,base:sm:bg-*, andbase:dark:bg-*, even while the condition matches. Write state styles withoutbase:. - 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, butbefore:base:bg-*compiles to an invalid selector and never applies. !importantflips the layer order:base:bg-blue-600!beatsbg-violet-600!.
For runtime conflict resolution, cn and tailwind-merge remove earlier conflicting utilities from the class string.
tailwind-merge
Section titled “tailwind-merge”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:
pnpm add tailwind-mergeimport { 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
twMergeUse 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 either configuration
Section titled “Use either configuration”Use the configured functions throughout your project:
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.