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"

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"

A variant name prefixed with _ is internal: it’s still declarable in variants, drivable via defaultVariants, and matchable in compoundVariants, but hidden from the component’s props type and from getSchema.

Use this for a variant an author drives (state, composition wiring), not one a consumer of the component should set directly.

import { cva } 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",
},
});
button({ size: "small" });
// => "button button--primary button--small"
button({ _intent: "secondary" });
// Type error: object literal may only specify known properties, and
// '_intent' does not exist in type '{ size?: "small" | "medium" }'

A composed-only internal variant is hidden from the composer’s props 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"