Skip to main content

Quickstart

1

Install CVA

npm i class-variance-authority
2

Create your first variant component

Define a button component with cva. Pass base classes as the first argument, then describe your variants, compoundVariants, and defaultVariants in the config object.
components/button.ts
import { cva } from "class-variance-authority";

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: {
      small: ["text-sm", "py-1", "px-2"],
      medium: ["text-base", "py-2", "px-4"],
    },
    disabled: {
      false: null,
      true: ["opacity-50", "cursor-not-allowed"],
    },
  },
  compoundVariants: [
    {
      intent: "primary",
      disabled: false,
      class: "hover:bg-blue-600",
    },
    {
      intent: "secondary",
      disabled: false,
      class: "hover:bg-gray-100",
    },
    {
      intent: "primary",
      size: "medium",
      class: "uppercase",
    },
  ],
  defaultVariants: {
    intent: "primary",
    size: "medium",
    disabled: false,
  },
});
Calling the component resolves the correct class string:
button();
// => "font-semibold border rounded bg-blue-500 text-white border-transparent text-base py-2 px-4 hover:bg-blue-600 uppercase"

button({ disabled: true });
// => "font-semibold border rounded bg-blue-500 text-white border-transparent text-base py-2 px-4 opacity-50 cursor-not-allowed uppercase"

button({ intent: "secondary", size: "small" });
// => "font-semibold border rounded bg-white text-gray-800 border-gray-400 text-sm py-1 px-2 hover:bg-gray-100"
Use of Tailwind CSS is optional. CVA works with any class-based styling approach, including plain CSS class names and CSS Modules.
3

Use it in your framework

cva returns a plain string, so it works anywhere you can set a class attribute.
components/Button.tsx
import { button } from "./button";

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  intent?: "primary" | "secondary";
  size?: "small" | "medium";
}

export function Button({ intent, size, className, ...props }: ButtonProps) {
  return (
    <button
      className={button({ intent, size, className })}
      {...props}
    />
  );
}
4

Extract variant types with VariantProps

Use the VariantProps utility type to derive the TypeScript prop types directly from your cva definition. This keeps your types and your variant config in sync automatically.
components/button.ts
import { cva, type VariantProps } from "class-variance-authority";

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: {
      small: ["text-sm", "py-1", "px-2"],
      medium: ["text-base", "py-2", "px-4"],
    },
  },
  defaultVariants: {
    intent: "primary",
    size: "medium",
  },
});

// Extract the variant prop types
export type ButtonVariants = VariantProps<typeof button>;
// => { intent?: "primary" | "secondary"; size?: "small" | "medium" }
You can then use ButtonVariants in your component props interface:
components/Button.tsx
import { button, type ButtonVariants } from "./button";

interface ButtonProps
  extends ButtonVariants,
    React.ButtonHTMLAttributes<HTMLButtonElement> {}

export function Button({ intent, size, ...props }: ButtonProps) {
  return <button className={button({ intent, size })} {...props} />;
}

Next steps

Core concepts: Variants

Learn about variants, compound variants, boolean variants, and default variants in depth.

API Reference

Full reference for cva, cx, compose, defineConfig, and VariantProps.

Build docs developers (and LLMs) love