Skip to content

What's New?

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

What’s changed since class-variance-authority@0.*?

  • Install cva@beta.
  • TypeScript 6.0 or later for TypeScript projects.
  • Tailwind CSS v4 for the optional cva/tailwindcss stylesheet.

One of the biggest (and let’s be honest, most important) complaints about class-variance-authority was that the name was just too damn long.

Shout-out to GitHub for transferring npm ownership of cva!

2. Bring your own concatenator with cva/config

Section titled “2. Bring your own concatenator with cva/config”

cva and cx still use clsx by default. For your own setup, use defineConfig from cva/config. Its required cx option accepts cn or tailwind-merge, clsx/lite for string-only classes, or your own concatenator.

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

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

Import cva/tailwindcss in a Tailwind CSS v4 project and prefix component defaults with base:. Ordinary utilities override them through the CSS cascade, with no runtime class merging.

Component calls are up to 1000% faster than class-variance-authority@0.7. cva prepares class lookups when you create the component and runs your concatenator once per call:

  • Components with variants and compound variants: 300% to 550% faster per call.
  • Matching a dozen compound variants: 700% to 1000% faster.
  • One component called with 24 different prop shapes: 400% faster.
  • Components with only a base: 50% faster.

The cva entry is 1.64 KB (brotli), measured from the current build.

Thanks to @fveracoechea for inspiring this work! See the runtime benchmarks for the full results.

TypeScript does less work without changing inferred props, getSchema results, or emitted declarations. The type-checking benchmarks compare against the preceding beta revision using tsc --extendedDiagnostics:

  • A project with 200 components type-checks about 10% faster, with 26% fewer type instantiations.
  • Composed components: 16% fewer instantiations. Components with a getSchema call: 24% fewer.

Prefix a variant name with _ to make it internal. VariantProps and getSchema omit it, but you can still pass it directly to the cva component.

Shallow merge one or more cva components with the composes property.

This replaces the compose function from earlier betas.

getSchema from cva/tools returns a plain object of variant names, values, and defaults. Use it to generate Storybook controls or variant galleries from your component.

Two Agent Skills help your coding agent work with cva:

The Tailwind CSS IntelliSense setup now uses classFunctions instead of classRegex. It covers Visual Studio Code, Zed, Neovim, and WebStorm, and works with class-variance-authority too.

Pass base styles in the config object’s base property:

import { cva } from "class-variance-authority";
import { cva } from "cva";
const component = cva("your-base-class");
const component = cva({ base: "your-base-class" });

The config object is required. Replace cva() with cva({}) to avoid a TypeScript error.

Passing null used to disable a variant, matching Stitches.js. This caused a great deal of confusion.

TypeScript now rejects null in props, defaultVariants, and compoundVariants selectors. At runtime, a null prop applies the variant’s default class instead of disabling it. Compound variants still compare the null as-is, so it isn’t interchangeable with undefined.

Use an explicit unset variant instead:

const button = cva({
base: "button",
variants: { size: { small: "button--small" } },
variants: { size: { unset: null, small: "button--small" } },
defaultVariants: { size: "small" },
});
button({ size: null });
button({ size: "unset" });
// => "button"

cva infers variant types from your config. Passing a generic type argument now produces a TypeScript error.

cva has no types entry point. Import ClassValue, ClassArray, and ClassDictionary from cva itself, alongside VariantProps.

5. Configuration is read when you create the component

Section titled “5. Configuration is read when you create the component”

cva now reads base, variants, compoundVariants, defaultVariants, and composes when you create the component, which accounts for most of the runtime speed improvement. Calls read your props and the current cx from defineConfig.

This change only affects earlier cva betas. class-variance-authority never had compose.

cva@1.0 removes the deprecated compose function. Use composes inside cva instead, passing a single component or an array:

import { cva, compose } from "cva";
import { cva } from "cva";
const box = cva({ /* ... */ });
const root = cva({ /* ... */ });
const card = compose(box, root);
const card = cva({ composes: [box, root] });

Use the cva-migrate Agent Skill to upgrade from class-variance-authority@0.x or an earlier cva beta.