Skip to content

Variants

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

To kick things off, let’s build a “basic” button component, using cva to handle our variant’s classes

components/button.ts
import { cva } from "cva";
const button = cva({
base: "rounded border font-semibold",
// **or**
// base: ["font-semibold", "border", "rounded"],
variants: {
intent: {
primary: "border-transparent bg-blue-500 text-white hover:bg-blue-600",
// **or**
// primary: [
// "bg-blue-500",
// "text-white",
// "border-transparent",
// "hover:bg-blue-600",
// ],
secondary: "border-gray-400 bg-white text-gray-800 hover:bg-gray-100",
},
size: {
small: "px-2 py-1 text-sm",
medium: "px-4 py-2 text-base",
},
},
compoundVariants: [
{
intent: "primary",
size: "medium",
class: "uppercase",
// **or** if you're a React.js user, `className` may feel more consistent:
// className: "uppercase"
},
],
defaultVariants: {
intent: "primary",
size: "medium",
},
});
button();
// => "rounded border font-semibold border-transparent bg-blue-500 text-white hover:bg-blue-600 px-4 py-2 text-base uppercase"
button({ intent: "secondary", size: "small" });
// => "rounded border font-semibold border-gray-400 bg-white text-gray-800 hover:bg-gray-100 px-2 py-1 text-sm"

A variant name prefixed with _ is treated as internal.

Internal variants remain available to the component. You can set them via defaultVariants and match them in compoundVariants. Both VariantProps and getSchema omit them.

import { cva, type VariantProps } from "cva";
const button = cva({
base: "button",
variants: {
_intent: { primary: "button--primary", secondary: "button--secondary" },
size: { small: "button--small", medium: "button--medium" },
},
defaultVariants: {
_intent: "primary",
size: "medium",
},
});
// `_intent` is omitted from the public props.
type ButtonProps = VariantProps<typeof button>;
// => { size?: "small" | "medium" | undefined }
// The component still accepts `_intent` directly.
button({ _intent: "secondary", size: "small" });
// => "button button--secondary button--small"

In a React component, extend VariantProps for the public props, then set the internal variant yourself:

interface Props
extends
React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof button> {
active?: boolean;
}
// `_intent` never appears in `Props`, so a consumer can't set it.
function Button({ active, size, className, ...props }: Props) {
return (
<button
className={button({
size,
_intent: active ? "primary" : "secondary",
class: className,
})}
{...props}
/>
);
}

A composed-only internal variant is omitted from the composer’s VariantProps too. To change its default from the composer, redeclare it locally: the same rule applies to any composed-only variant (see Extending variants).

const base = cva({
variants: {
_tone: { quiet: "tone-quiet", loud: "tone-loud" },
},
defaultVariants: { _tone: "quiet" },
});
const card = cva({
composes: base,
variants: { _tone: { loud: "loud-local" } },
defaultVariants: { _tone: "loud" },
});
card();
// => "tone-loud loud-local"

Variants that apply when multiple other variant conditions are met.

components/button.ts
import { cva } from "cva";
const button = cva({
base: "",
variants: {
intent: { primary: "", secondary: "" },
size: { small: "", medium: "" },
},
compoundVariants: [
// Applied via:
// `button({ intent: "primary", size: "medium" })`
{
intent: "primary",
size: "medium",
class: "",
},
],
});
components/button.ts
import { cva } from "cva";
const button = cva({
base: "",
variants: {
intent: { primary: "", secondary: "" },
size: { small: "", medium: "" },
},
compoundVariants: [
// Applied via:
// `button({ intent: "primary", size: "medium" })`
// or
// `button({ intent: "secondary", size: "medium" })`
{
intent: ["primary", "secondary"],
size: "medium",
class: "",
},
],
});

To disable a variant completely, provide an option with a value of null.

If you’re stuck on naming, we recommend setting an explicit "unset" option (similar to the CSS keyword).

import { cva } from "cva";
const button = cva({
base: "button",
variants: {
intent: {
unset: null,
primary: "button--primary",
secondary: "button--secondary",
},
},
});
button({ intent: "unset" });
// => "button"