Class Variance Authority (CVA)
Class Variance Authority (CVA) is a tiny, TypeScript-first utility library for defining and resolving UI component variants in a structured, type-safe manner. It allows developers to describe base classes and variant-specific classes, handling the runtime resolution based on provided props. CVA supports features like compound variants, default variants, and provides a `VariantProps` utility type for extracting TypeScript prop types. Currently, the stable package is `class-variance-authority`, with version 0.7.1 as of the latest npm publish over a year ago. While the `cva` package name exists, it's a placeholder, and the official project intends to use `cva` as the primary name from v1 onwards. It is framework-agnostic (works with React, Vue, Svelte, plain HTML) and CSS-agnostic (Tailwind CSS, CSS Modules, plain classes), making it highly flexible. Its small bundle size (approx. 1.6 KB minified + gzipped) and lack of runtime style injection are key differentiators against CSS-in-JS solutions, providing full control over stylesheet output.
Common errors
-
Cannot find module 'cva' or its corresponding type declarations.
cause Attempting to import from the `cva` package name, which is a placeholder, instead of `class-variance-authority`.fixEnsure you have installed `class-variance-authority` (`npm install class-variance-authority`) and your import statement is `import { cva } from 'class-variance-authority';`. -
Argument of type '{ variant: string; }' is not assignable to parameter of type 'VariantProps<typeof buttonVariants> | undefined'.cause Passing an invalid variant value that is not defined in your `cva` configuration, or a typo in the variant name.fixCheck your `cva` definition to ensure the variant name and its possible values match what you are passing. TypeScript helps catch these at compile time; ensure your editor shows the correct type suggestions.
Warnings
- gotcha The official package name for Class Variance Authority is `class-variance-authority`, not `cva`. The `cva` package on npm is an abandoned placeholder.
- breaking Future versions (specifically v1) are expected to transition the primary npm package name from `class-variance-authority` to `cva`. This will require updating import paths in your codebase.
- gotcha When using TypeScript, always use `type` import for `VariantProps` to ensure it's stripped from the compiled output.
Install
-
npm install cva -
yarn add cva -
pnpm add cva
Imports
- cva
import cva from 'class-variance-authority'; import { cva } from 'cva';import { cva } from 'class-variance-authority'; - VariantProps
import { VariantProps } from 'class-variance-authority';import { type VariantProps } from 'class-variance-authority'; - * (CommonJS)
const cva = require('class-variance-authority');const { cva } = require('class-variance-authority');
Quickstart
import { cva, type VariantProps } from 'class-variance-authority';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none',
{
variants: {
intent: {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
danger: 'bg-red-500 text-white hover:bg-red-600',
},
size: {
sm: 'h-9 px-3 py-2',
md: 'h-10 px-4 py-2',
lg: 'h-11 px-6 py-3',
},
},
compoundVariants: [
{ intent: 'primary', size: 'md', class: 'uppercase' },
{ intent: 'secondary', size: 'sm', class: 'font-normal' }
],
defaultVariants: {
intent: 'primary',
size: 'md',
},
}
);
type ButtonProps = VariantProps<typeof buttonVariants>;
// Example usage:
console.log(buttonVariants({ intent: 'primary', size: 'lg' }));
// Expected output: 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none bg-blue-600 text-white hover:bg-blue-700 h-11 px-6 py-3'
console.log(buttonVariants({ intent: 'secondary', size: 'sm' }));
// Expected output: 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none bg-gray-200 text-gray-900 hover:bg-gray-300 h-9 px-3 py-2 font-normal'
console.log(buttonVariants({}));
// Expected output (default variants): 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none bg-blue-600 text-white hover:bg-blue-700 h-10 px-4 py-2 uppercase'