Skip to content
Terminal window
pnpm i class-variance-authority
Do I have to write such a long package name?

Unfortunately, for a little bit longer, yes. Originally, the plan was to publish the package as cva, but someone had already registered that name on npm and marked it as a “placeholder”.

On 2022/02/16, GitHub transferred NPM ownership of cva to Joe Bell. This shorter name will be used from v1 onwards.

In the meantime, you can always alias the package for your convenience…

  1. Alias the package with npm install

    Terminal window
    npm i cva@npm:class-variance-authority
  2. Then import like so:

    import { cva } from "cva";
    // …

If you’re a Tailwind user, here are some additional (optional) steps to get the most out of cva:

You can enable autocompletion inside cva using the steps below:

  1. Install the “Tailwind CSS IntelliSense” Visual Studio Code extension

  2. Add the following to your settings.json:

{
"tailwindCSS.classFunctions": ["cva", "cx"]
}

By default, cva joins class names but does not resolve conflicting Tailwind CSS utilities. Choose one of the approaches below:

This Tailwind CSS v4 recipe adds a base: variant that marks a utility as a component default, so any ordinary utility overrides it through the CSS cascade instead of runtime class merging. Add it after Tailwind’s import:

@import "tailwindcss";
@custom-variant base {
@layer base {
:where(&) {
@slot;
}
}
}

Prefix your defaults with base: and write state styles as ordinary utilities:

import { cva } from "class-variance-authority";
const buttonVariants = cva(
"base:bg-blue-600 base:px-4 base:py-2 base:text-white hover:bg-blue-700",
);
buttonVariants({ 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.

base: makes defaults overridable. It does not merge classes, so keep these rules in mind:

  • Conditional defaults lose too: any ordinary bg-* utility beats base:hover:bg-*, base:sm:bg-*, and base:dark:bg-*, even while the condition matches. Write state styles without base:.
  • 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, but before:base:bg-* compiles to an invalid selector and never applies.
  • !important flips the layer order: base:bg-blue-600! beats bg-violet-600!.

If you’d rather not think about the cascade, tailwind-merge is more bulletproof: it merges classes at runtime, so the last conflicting class wins. Wrap your cva component with twMerge:

Example with tailwind-merge
import { cva, type VariantProps } from "class-variance-authority";
import { twMerge } from "tailwind-merge";
const buttonVariants = cva(["your", "base", "classes"], {
variants: {
intent: {
primary: ["your", "primary", "classes"],
},
},
defaultVariants: {
intent: "primary",
},
});
export interface ButtonVariants extends VariantProps<typeof buttonVariants> {}
export const button = (variants: ButtonVariants) =>
twMerge(buttonVariants(variants));