Skip to main content

Migrating

The cva package (v1+) is the successor to class-variance-authority (v0.x). It ships new features — compose and defineConfig — and changes the cva() call signature from positional arguments to a config object.

Package rename

The class-variance-authority package is no longer receiving new features. Migrate to cva to access compose, defineConfig, and future releases.
1

Uninstall the legacy package

npm uninstall class-variance-authority
2

Install the new package

npm install cva
3

Update imports

Replace all imports from class-variance-authority with cva.
-import { cva, cx, type VariantProps } from "class-variance-authority";
+import { cva, cx, type VariantProps } from "cva";
4

Update cva() call signatures

The legacy package accepted two positional arguments: cva(base, options). The new package accepts a single config object: cva({ base, variants, compoundVariants, defaultVariants }).
This is a breaking change. Every cva() call must be updated.
Before (class-variance-authority v0.x)
const button = cva(["font-semibold", "border", "rounded"], {
  variants: {
    intent: {
      primary: ["bg-blue-500", "text-white", "border-transparent"],
      secondary: ["bg-white", "text-gray-800", "border-gray-400"],
    },
    size: {
      sm: ["text-sm", "py-1", "px-2"],
      md: ["text-base", "py-2", "px-4"],
    },
  },
  compoundVariants: [
    { intent: "primary", size: "md", class: "uppercase" },
  ],
  defaultVariants: {
    intent: "primary",
    size: "md",
  },
});
After (cva v1+)
const button = cva({
  base: ["font-semibold", "border", "rounded"],
  variants: {
    intent: {
      primary: ["bg-blue-500", "text-white", "border-transparent"],
      secondary: ["bg-white", "text-gray-800", "border-gray-400"],
    },
    size: {
      sm: ["text-sm", "py-1", "px-2"],
      md: ["text-base", "py-2", "px-4"],
    },
  },
  compoundVariants: [
    { intent: "primary", size: "md", class: "uppercase" },
  ],
  defaultVariants: {
    intent: "primary",
    size: "md",
  },
});
The diff in one place:
-const button = cva(["font-semibold", "border", "rounded"], {
-  variants: { … },
-  compoundVariants: [ … ],
-  defaultVariants: { … },
-});
+const button = cva({
+  base: ["font-semibold", "border", "rounded"],
+  variants: { … },
+  compoundVariants: [ … ],
+  defaultVariants: { … },
+});

New features in cva v1+

compose

compose is a new helper that merges multiple CVA components into one. It is not available in class-variance-authority.
import { cva, compose } from "cva";

const box = cva({
  base: "box box-border",
  variants: {
    margin: { 0: "m-0", 4: "m-4" },
    padding: { 0: "p-0", 4: "p-4" },
  },
  defaultVariants: { margin: 0, padding: 0 },
});

const card = cva({
  base: "card border-solid border-slate-300 rounded",
  variants: {
    shadow: { md: "drop-shadow-md", lg: "drop-shadow-lg" },
  },
});

const cardBox = compose(box, card);

cardBox({ margin: 4, padding: 4, shadow: "md" });
// => "box box-border m-4 p-4 card border-solid border-slate-300 rounded drop-shadow-md"
See the compose API reference for full documentation.

defineConfig

defineConfig lets you configure a custom onComplete hook that runs after every class concatenation — the primary use case is integrating tailwind-merge globally:
lib/utils.ts
import { defineConfig } from "cva";
import { twMerge } from "tailwind-merge";

export const { cva, cx, compose } = defineConfig({
  hooks: {
    onComplete: (className) => twMerge(className),
  },
});
See the defineConfig API reference for full documentation.

Build docs developers (and LLMs) love