cva@beta: Abridged documentation for the cva@beta release (https://cva.style/beta)
# Class Variance Authority
> Build type-safe, variant-driven class names for any styling approach, with first-class Tailwind CSS support.
 `cva` is a tiny (1.62 KB compressed) library for building type-safe, variant-driven class names with Tailwind CSS or any other styling approach. CSS-in-TS libraries such as [Stitches](https://stitches.dev/docs/variants) and [Vanilla Extract](https://vanilla-extract.style/documentation/api/style-variants/) handle type-safe UI variants without you managing class names or stylesheet composition by hand. CSS-in-TS isn’t for everyone, though. You may need full control over your stylesheet output, use a framework such as Tailwind CSS, or prefer writing your own CSS. Creating variants with the “traditional” CSS approach can become an arduous task: manually matching classes to props, and manually adding types. `cva` takes away those pain points, so you can focus on building your UI. ## Sponsors [Section titled “Sponsors”](#sponsors) ## Acknowledgments [Section titled “Acknowledgments”](#acknowledgments) * [**Stitches**](https://stitches.dev/) ([WorkOS](https://workos.com))\ Huge thanks to the WorkOS team for pioneering the `variants` API movement: your open-source contributions are immensely appreciated * [**cx**](https://github.com/joe-bell/cx)\ Some of the ideas behind `cva` first appeared in `cx`, a type-safe class concatenation utility with reusable shortcuts, in October 2020. * [**clb**](https://github.com/crswll/clb) ([Bill Criswell](https://github.com/crswll))\ `cva` began with the intention of merging into the wonderful [`clb`](https://github.com/crswll/clb) library, but after some discussion with Bill, we felt a separate project made more sense.\ I’m so grateful to Bill for sharing his work publicly and for getting me excited about building a type-safe variants API for classes. If you have a moment, please go and [star the project on GitHub](https://github.com/crswll/clb). Thank you Bill! * [**clsx**](https://github.com/lukeed/clsx) ([Luke Edwards](https://github.com/lukeed))\ Previously, this project surfaced a custom `cx` utility for flattening classes, but it lacked the ability to handle variadic arguments or objects. [clsx](https://github.com/lukeed/clsx) provided those extra features with quite literally zero increase to the bundle size: a no-brainer to switch! * [**Vanilla Extract**](http://vanilla-extract.style) ([Seek](https://github.com/seek-oss)) ## Downloads [Section titled “Downloads”](#downloads) * [Wallpaper](/assets/img/wallpaper-4k.png) ## License [Section titled “License”](#license) [Apache-2.0 License](https://github.com/joe-bell/cva/blob/main/LICENSE) © [Joe Bell](https://joebell.studio)
# API Reference
> API reference for cva, cva/config, and cva/tools.
## `cva` [Section titled “cva”](#cva) Builds a `cva` component
```ts
import { cva } from "cva";
const component = cva(options);
```
`cva` reads `options` during creation. Treat it, and everything it references, as immutable afterwards: to change a component’s configuration, create a new component. A call reads only the props you pass it, plus the `cx` you gave [`defineConfig`](#defineconfig), which stays live. Getters with side effects, and changing the props object while the call is running, are unsupported. ### Parameters [Section titled “Parameters”](#parameters) 1. `options` * `base`: the base class name (`string`, `string[]` or other [`clsx` value](https://github.com/lukeed/clsx#input)) * `variants`: your [variants schema](/beta/getting-started/variants) * `compoundVariants`: variants based on a combination of previously defined variants * `defaultVariants`: set default values for previously defined variants * `composes`: shallow merge one or more other `cva` components into this one, as a single component or an array (see [Composing Components](/beta/getting-started/composing-components)) ### Returns [Section titled “Returns”](#returns) A `cva` component function ## `cx` [Section titled “cx”](#cx) Concatenates class names (an alias of [`clsx`](https://github.com/lukeed/clsx); swap in your own concatenator via [`cva/config`](#cvaconfig)).
```ts
import { cx } from "cva";
const className = cx(classes);
```
### Parameters [Section titled “Parameters”](#parameters-1) * `classes`: zero or more class values. The preset accepts [`clsx` values](https://github.com/lukeed/clsx#input); `cva/config` uses the configured concatenator’s grammar ### Returns [Section titled “Returns”](#returns-1) `string` ## `cva/tools` [Section titled “cva/tools”](#cvatools) ### `getSchema` [Section titled “getSchema”](#getschema) Extracts a plain-object schema (variant names, possible values, and default values) from a `cva` component. Use it to generate Storybook controls, documentation, or any other UI that reads a component’s variants without re-declaring them. See [Tools](/beta/getting-started/tools) for use cases.
```ts
import { cva } from "cva";
import { getSchema } from "cva/tools";
const button = cva({
base: "button",
variants: {
intent: { primary: "button--primary", secondary: "button--secondary" },
disabled: { true: "button--disabled", false: "button--enabled" },
},
defaultVariants: { intent: "primary", disabled: false },
});
getSchema(button);
// => {
// intent: { values: ["primary", "secondary"], defaultValue: "primary" },
// disabled: { values: [true, false], defaultValue: false },
// }
```
`getSchema` omits [internal variants](/beta/getting-started/variants#internal-variants) or variants that have no values (e.g. `variants: { empty: {} }`). #### Parameters [Section titled “Parameters”](#parameters-2) `component`: a component created by `cva` (including components composed via [`composes`](#cva)) #### Returns [Section titled “Returns”](#returns-2) An object keyed by variant name. Each entry has: * `values`: a readonly array of the variant’s possible values * `defaultValue`: present only if the variant has a `defaultVariants` entry ## `cva/config` [Section titled “cva/config”](#cvaconfig) The `cva` package is a preset: [`cva`](#cva) and [`cx`](#cx) use `clsx`. `cva/config` is the same engine without a configured concatenator. ### `defineConfig` [Section titled “defineConfig”](#defineconfig) Generate `cva` and `cx` functions based on your preferred configuration. Store in a `cva.config.ts` file, and import across your project. cva.config.ts
```ts
import { defineConfig } from "cva/config";
export const { cva, cx } = defineConfig(options);
```
1. `options` * `cx` (**required**): the class name concatenator used by `cva` and `cx` * It owns the class name grammar: `cva` passes composed outputs, `base`, matched variant and compound-variant values, and `class`/`className` through verbatim, one argument each. * Custom callbacks must accept empty calls, variadic inputs, and composed component strings. * The authoring surface adopts its parameter type: `twMerge` rejects object syntax, while `clsx` keeps the full clsx-flavored `ClassValue` grammar. * See [Merging classes](/beta/getting-started/installation#merging-classes) for `cn` and `tailwind-merge` examples. ### Advanced concatenators [Section titled “Advanced concatenators”](#advanced-concatenators) Use [`clsx/lite`](https://github.com/lukeed/clsx#clsxlite) only when every authored value is a string and you do not need conflict resolution. Its runtime ignores arrays, objects, and numbers even though its published types accept them. Annotate a string-only wrapper so `cva` rejects those values too:
```ts
import { defineConfig } from "cva/config";
import { clsx as clsxLite } from "clsx/lite";
export const { cva, cx } = defineConfig({
cx: (...inputs: string[]) => clsxLite(...inputs),
});
```
# 11ty
> Build a cva button component in an 11ty template with Tailwind CSS.
This example builds a `button` component with `cva` in an 11ty template. ## Tailwind CSS [Section titled “Tailwind CSS”](#tailwind-css) button.11ty.js
```js
const { cva } = require("cva");
// ⚠️ Disclaimer: Use of Tailwind CSS is optional
const button = cva({
base: "button",
variants: {
intent: {
primary: [
"bg-blue-500 text-white border-transparent",
"hover:bg-blue-600",
],
secondary: [
"bg-white text-gray-800 border-gray-400",
"hover:bg-gray-100",
],
},
size: {
small: "py-1 px-2 text-sm",
medium: "py-2 px-4 text-base",
},
},
compoundVariants: [{ intent: "primary", size: "medium", class: "uppercase" }],
defaultVariants: {
intent: "primary",
size: "medium",
},
});
module.exports = function ({ label, intent, size }) {
return ``;
};
```
# Astro
> A cva button component built with Astro and Tailwind CSS.
This example builds a `button` component with `cva` in an Astro project. ## Tailwind CSS [Section titled “Tailwind CSS”](#tailwind-css) [View source on GitHub ↗](https://github.com/joe-bell/cva/tree/main/examples/beta/astro-with-tailwindcss/src/components/button.astro) Sorry, your browser [doesn't support](https://developer.stackblitz.com/platform/webcontainers/browser-support) this type of embed. Try [visiting the example directly](https://stackblitz.com/github/joe-bell/cva/tree/main/examples/beta/astro-with-tailwindcss?file=src/components/button.astro) or reopen this page in a Chromium-based browser.
# BEM
> Use cva to apply BEM class names instead of utility classes.
This example applies BEM class names with `cva` instead of utility classes. styles.css
```css
.button {
/* */
}
.button--primary {
/* */
}
.button--secondary {
/* */
}
.button--small {
/* */
}
.button--medium {
/* */
}
.button--primary-medium {
/* */
}
```
```ts
import { cva } from "cva";
const button = cva({
base: "button",
variants: {
intent: {
primary: "button--primary",
secondary: "button--secondary",
},
size: {
small: "button--small",
medium: "button--medium",
},
},
compoundVariants: [
{ intent: "primary", size: "medium", class: "button--primary-medium" },
],
defaultVariants: {
intent: "primary",
size: "medium",
},
});
button();
// => "button button--primary button--medium button--primary-medium"
button({ intent: "secondary", size: "small" });
// => "button button--secondary button--small"
```
# Other Use Cases
> Use cva to manage variant-driven strings beyond class names, such as dynamic text content.
Although primarily designed for handling class names, at its core `cva` is a fancy way of managing a string… ## Dynamic text content [Section titled “Dynamic text content”](#dynamic-text-content)
```ts
import { cva } from "cva";
const greeter = cva({
base: "Good morning!",
variants: {
isLoggedIn: {
true: "Here's a secret only logged in users can see",
false: "Log in to find out more…",
},
},
defaultVariants: {
isLoggedIn: false,
},
});
greeter();
// => "Good morning! Log in to find out more…"
greeter({ isLoggedIn: true });
// => "Good morning! Here's a secret only logged in users can see"
```
# React with CSS Modules
> A cva button component styled with CSS Modules in React.
This example builds a `button` component with `cva`, styled with CSS Modules, in a React project. [View source on GitHub ↗](https://github.com/joe-bell/cva/tree/main/examples/beta/react-with-css-modules/src/components/button/button.tsx) Sorry, your browser [doesn't support](https://developer.stackblitz.com/platform/webcontainers/browser-support) this type of embed. Try [visiting the example directly](https://stackblitz.com/github/joe-bell/cva/tree/main/examples/beta/react-with-css-modules?file=src/components/button/button.tsx) or reopen this page in a Chromium-based browser.
# React with Tailwind CSS
> A cva button component built with React and Tailwind CSS, including compound components and class conflict resolution.
These examples build a React `button` component with `cva` and Tailwind CSS. They cover compound components and conflicting Tailwind classes. ## Basic component [Section titled “Basic component”](#basic-component) [View source on GitHub ↗](https://github.com/joe-bell/cva/tree/main/examples/beta/react-with-tailwindcss/src/components/button/button.tsx) Sorry, your browser [doesn't support](https://developer.stackblitz.com/platform/webcontainers/browser-support) this type of embed. Try [visiting the example directly](https://stackblitz.com/github/joe-bell/cva/tree/main/examples/beta/react-with-tailwindcss?file=src/components/button/button.tsx) or reopen this page in a Chromium-based browser. ## Compound components [Section titled “Compound components”](#compound-components) [View source on GitHub ↗](https://github.com/joe-bell/cva/tree/main/examples/beta/react-with-tailwindcss-compound/src/components/nav/nav.tsx) Sorry, your browser [doesn't support](https://developer.stackblitz.com/platform/webcontainers/browser-support) this type of embed. Try [visiting the example directly](https://stackblitz.com/github/joe-bell/cva/tree/main/examples/beta/react-with-tailwindcss-compound?file=src/components/nav/nav.tsx) or reopen this page in a Chromium-based browser. ## Merging classes [Section titled “Merging classes”](#merging-classes) Both examples resolve conflicting Tailwind CSS classes and conditional inputs. See [Merging classes](/beta/getting-started/installation#merging-classes) for the configurations. ### cn [Section titled “cn”](#cn) [`cn`](https://www.npmjs.com/package/cn) joins conditional classes and resolves Tailwind CSS conflicts in one function. [View source on GitHub ↗](https://github.com/joe-bell/cva/tree/main/examples/beta/react-with-cn/src/cva.config.ts) Sorry, your browser [doesn't support](https://developer.stackblitz.com/platform/webcontainers/browser-support) this type of embed. Try [visiting the example directly](https://stackblitz.com/github/joe-bell/cva/tree/main/examples/beta/react-with-cn?file=src/cva.config.ts) or reopen this page in a Chromium-based browser. ### tailwind-merge [Section titled “tailwind-merge”](#tailwind-merge) [`clsx`](https://github.com/lukeed/clsx) with [`tailwind-merge`](https://github.com/dcastil/tailwind-merge) preserves `cva`’s full `ClassValue` grammar while resolving Tailwind CSS conflicts. [View source on GitHub ↗](https://github.com/joe-bell/cva/tree/main/examples/beta/react-with-tailwind-merge/src/cva.config.ts) Sorry, your browser [doesn't support](https://developer.stackblitz.com/platform/webcontainers/browser-support) this type of embed. Try [visiting the example directly](https://stackblitz.com/github/joe-bell/cva/tree/main/examples/beta/react-with-tailwind-merge?file=src/cva.config.ts) or reopen this page in a Chromium-based browser.
# Svelte
> A cva button component built with Svelte.
This example builds a `button` component with `cva` in a Svelte project. Open the sandbox below to explore the full source. [View source on GitHub ↗](https://github.com/joe-bell/cva/tree/main/examples/beta/svelte/src/components/button.svelte) Sorry, your browser [doesn't support](https://developer.stackblitz.com/platform/webcontainers/browser-support) this type of embed. Try [visiting the example directly](https://stackblitz.com/github/joe-bell/cva/tree/main/examples/beta/svelte?file=src/components/button.svelte) or reopen this page in a Chromium-based browser.
# Vue
> A cva button component built with Vue.
This example builds a `Button` component with `cva` in a Vue project. Open the sandbox below to explore the full source. [View source on GitHub ↗](https://github.com/joe-bell/cva/tree/main/examples/beta/vue/src/components/Button.vue) Sorry, your browser [doesn't support](https://developer.stackblitz.com/platform/webcontainers/browser-support) this type of embed. Try [visiting the example directly](https://stackblitz.com/github/joe-bell/cva/tree/main/examples/beta/vue?file=src/components/Button.vue) or reopen this page in a Chromium-based browser.
# FAQs
> Answers to common questions about cva's API design, including responsive variants and styled-component APIs.
## Why don’t you provide a `styled` API? [Section titled “Why don’t you provide a styled API?”](#why-dont-you-provide-a-styled-api) Long story short: it’s unnecessary. `cva` encourages you to think of components as traditional CSS classes: * Less JavaScript is better * They’re framework agnostic; truly reusable * Polymorphism is free: apply the class to your preferred HTML element * Less opinionated; you’re free to build components with `cva` however you’d like See the [“Polymorphism”](/beta/getting-started/polymorphism) documentation for further recommendations. ## How can I create [responsive variants like Stitches.js](https://stitches.dev/docs/responsive-styles#responsive-variants)? [Section titled “How can I create responsive variants like Stitches.js?”](#how-can-i-create-responsive-variants-like-stitchesjs) You can’t. `cva` doesn’t know about how you choose to apply CSS classes, and it doesn’t want to. We recommend either: * Showing/hiding elements with different variants, based on your preferred breakpoint. - Create a bespoke variant that changes based on the breakpoint. *e.g. `button({ intent: "primaryUntilMd" })`* This is something I’ve been thinking about since the project’s inception, and I’ve gone back and forth many times on the idea of building it. It’s a large undertaking and brings all the complexity of supporting many different build tools and frameworks. In my experience, “responsive variants” are typically rare, and hiding/showing different elements is usually good enough to get by. To be frank, I’m probably not going to build/maintain a solution unless someone periodically gives me a thick wad of cash to do so, and even then I’d probably rather spend my free time living my life.
# Composing Components
> Merge one or more cva components into a single component with the composes property.
Shallow merge one or more `cva` components into a single component with the `composes` property. Pass a single component directly, or pass multiple components as an array: components/card.ts
```ts
import { cva, type VariantProps } from "cva";
const box = cva({
base: "box box-border",
variants: {
margin: { 0: "m-0", 2: "m-2", 4: "m-4", 8: "m-8" },
padding: { 0: "p-0", 2: "p-2", 4: "p-4", 8: "p-8" },
},
defaultVariants: {
margin: 0,
padding: 0,
},
});
const root = cva({
base: "card rounded border-solid border-slate-300",
variants: {
shadow: {
md: "drop-shadow-md",
lg: "drop-shadow-lg",
xl: "drop-shadow-xl",
},
},
});
export const card = cva({ composes: [box, root] });
export interface CardProps extends VariantProps {}
card({ margin: 2, shadow: "md" });
// => "box box-border m-2 p-0 card rounded border-solid border-slate-300 drop-shadow-md"
card({ margin: 2, shadow: "md", class: "adhoc-class" });
// => "box box-border m-2 p-0 card rounded border-solid border-slate-300 drop-shadow-md adhoc-class"
```
## Extending variants [Section titled “Extending variants”](#extending-variants) If more than one composed component declares the same variant, their values combine. Each component still resolves and applies its own class, so overlapping values extend one another rather than override:
```ts
const a = cva({ base: "a", variants: { style: { primary: "a-primary" } } });
const b = cva({
base: "b",
variants: { style: { primary: "b-primary", secondary: "b-secondary" } },
});
const combined = cva({ composes: [a, b] });
combined({ style: "primary" });
// => "a a-primary b b-primary"
```
`defaultVariants` follow a last-wins merge. If multiple composed components declare a default for the same variant, the last one in the array wins. A local `defaultVariants` on the composing component wins over all of them. `cva` applies that value to every composed component, not just the one that declared it:
```ts
const a = cva({
base: "a",
variants: { style: { primary: "a-primary" } },
defaultVariants: { style: "primary" },
});
const b = cva({
base: "b",
variants: { style: { primary: "b-primary", secondary: "b-secondary" } },
defaultVariants: { style: "secondary" },
});
const combinedWithDefaults = cva({ composes: [a, b] });
combinedWithDefaults();
// => "a b b-secondary"
```
The composing component can set `defaultVariants` and `compoundVariants` for composed variants without redeclaring them. Both are typechecked against the merged variants, so only values a composed component declares are accepted:
```ts
const combined = cva({
composes: [a, b],
compoundVariants: [{ style: "primary", class: "combined-primary" }],
defaultVariants: { style: "primary" },
});
combined();
// => "a a-primary b b-primary combined-primary"
```
Caution Pass components to `composes` as an inline array literal, or one marked `as const`. A pre-declared, mutable array variable (`const list = [a, b]`) loses the tuple inference `composes` relies on, which can silently widen or drop variant types. `cva` reads the array once, when you create the component, so pushing another component onto it afterwards has no effect. Create the parent again with the full list instead.
# Compound Components
> Build compound component sets, such as Accordion.Item, with cva and CSS.
For larger, more complex components, you may end up wanting to create a set of composable components that work together: “Compound Components” `cva` encourages you to build these compound components with CSS: leverage the cascade, custom properties, `:has()` selectors, and more… ## Examples [Section titled “Examples”](#examples) * [React with Tailwind CSS (Compound Components)](/beta/examples/react/tailwindcss#compound-components)
# Extending Components
> Pass extra classes to a cva component with the class or className prop.
All `cva` components provide an optional `class` **or** `className` prop, which can be used to pass additional classes to the component. components/button.ts
```ts
import { cva } from "cva";
const button = cva({ base: "font-semibold" });
button({ class: "m-4" });
// => "font-semibold m-4"
button({ className: "m-4" });
// => "font-semibold m-4"
```
# Installation
> Install cva@beta, configure Tailwind CSS IntelliSense, and merge conflicting utility classes.
* pnpm
```sh
pnpm i cva@beta
```
* npm
```sh
npm i cva@beta
```
* yarn
```sh
yarn add cva@beta
```
* bun
```sh
bun add cva@beta
```
* deno
```sh
deno add cva@beta
```
## Tailwind CSS [Section titled “Tailwind CSS”](#tailwind-css) If you’re a Tailwind user, here are some additional (optional) steps to get the most out of `cva`: ### IntelliSense [Section titled “IntelliSense”](#intellisense) You can enable autocompletion inside `cva` using the steps below: * Visual Studio Code 1. [Install the “Tailwind CSS IntelliSense” Visual Studio Code extension](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) 2. Add the following to your [`.vscode/settings.json`](https://code.visualstudio.com/docs/getstarted/settings): .vscode/settings.json
```json
{
"tailwindCSS.classFunctions": ["cva", "cx"],
}
```
* Zed Add the following to your [`.zed/settings.json`](https://zed.dev/docs/configuring-zed#settings-files): .zed/settings.json
```json
{
"lsp": {
"tailwindcss-language-server": {
"settings": {
"classFunctions": ["cva", "cx"],
}
}
}
}
```
* Neovim 1. [Install the extension](https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md#tailwindcss) 2. Add the following configuration:
```lua
require 'lspconfig'.tailwindcss.setup({
settings = {
tailwindCSS = {
classFunctions = { "cva", "cx" },
},
},
})
```
* WebStorm 1. Check the version. Available for [WebStorm 2023.1](https://www.jetbrains.com/webstorm/whatsnew/#version-2023-1-tailwind-css-configuration) and later 2. Open the settings. Go to [Languages and Frameworks | Style Sheets | Tailwind CSS](https://www.jetbrains.com/help/webstorm/tailwind-css.html#ws_css_tailwind_configuration) 3. Add the following to your tailwind configuration
```json
{
"classFunctions": ["cva", "cx"]
}
```
## Merging classes [Section titled “Merging classes”](#merging-classes) By default, `cva` uses [`clsx`](https://github.com/lukeed/clsx) for conditional class joining. It does not resolve conflicting Tailwind CSS utilities. Both configurations accept conditional objects and arrays. ### cn [Section titled “cn”](#cn) Install the [`cn`](https://www.npmjs.com/package/cn) package to join conditional classes and resolve Tailwind CSS conflicts in one function. The [shadcn CLI](https://ui.shadcn.com/docs/registry/github) installs `cva@beta` with `cn` and writes `cva.config.ts` to your project root:
```sh
pnpm dlx shadcn@latest add joe-bell/cva/cn
```
To set it up by hand instead, add `cn` and export the configured functions from `cva.config.ts`:
```sh
pnpm add cn
```
cva.config.ts
```ts
import { defineConfig } from "cva/config";
import { cn as merge } from "cn";
export const { cva, cx: cn } = defineConfig({ cx: merge });
```
### tailwind-merge [Section titled “tailwind-merge”](#tailwind-merge) Install [`clsx`](https://github.com/lukeed/clsx) with [`tailwind-merge`](https://github.com/dcastil/tailwind-merge) to preserve `cva`’s full `ClassValue` grammar while resolving Tailwind CSS conflicts:
```sh
pnpm add clsx tailwind-merge
```
cva.config.ts
```ts
import { defineConfig } from "cva/config";
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export const { cva, cx: cn } = defineConfig({
cx: (...inputs) => twMerge(clsx(inputs)),
});
```
### Use either configuration [Section titled “Use either configuration”](#use-either-configuration) Use the configured functions throughout your project: components/button.ts
```ts
import { cn, cva } from "../cva.config";
export const button = cva({
base: "font-semibold bg-gray-200 border rounded",
variants: {
intent: {
primary: "bg-blue-500 text-white border-transparent hover:bg-blue-600",
secondary: "bg-white text-gray-800 border-gray-400 hover:bg-gray-100",
},
},
defaultVariants: {
intent: "primary",
},
});
```
Both configurations produce the same output:
```ts
button();
// => "font-semibold border rounded bg-blue-500 text-white border-transparent hover:bg-blue-600"
cn("bg-gray-200", { "bg-blue-500": true });
// => "bg-blue-500"
```
For string-only authoring without conflict resolution, see [advanced concatenators](/beta/api-reference#advanced-concatenators) for `clsx/lite`.
# Polymorphism
> Apply cva classes to any HTML element, or roll your own render prop with Base UI's useRender.
`cva` components are polymorphic (and framework-agnostic) by default: apply the class to your preferred HTML element…
```tsx
import { button } from "./components/button";
export default () => (
Sign up
);
```
## Alternative approaches [Section titled “Alternative approaches”](#alternative-approaches) ### React [Section titled “React”](#react) If you’d prefer to use a React-based API, `cva` strongly recommends using [Base UI’s `useRender` hook](https://base-ui.com/react/utils/use-render) to roll your own `render` prop.
```tsx
"use client";
import { cva, type VariantProps } from "cva";
import { useRender } from "@base-ui/react/use-render";
import { mergeProps } from "@base-ui/react/merge-props";
const _BUTTON_DEFAULT_TAG = "button" satisfies React.ElementType;
export interface ButtonProps
extends
useRender.ComponentProps,
VariantProps {}
const button = cva({
base: "button",
variants: {
intent: {
primary: "bg-blue-500 hover:bg-blue-600 border-transparent text-white",
secondary: "border-gray-400 bg-white text-gray-800 hover:bg-gray-100",
},
},
});
export function Button({
render,
intent = "primary",
className,
...props
}: ButtonProps) {
const defaultProps: useRender.ElementProps = {
className: button({ intent, className }),
// Consider data-attributes for debugging purposes
["data-button" as string]: "",
["data-intent" as string]: intent,
};
return useRender({
defaultTagName: _BUTTON_DEFAULT_TAG,
render,
props: mergeProps(defaultProps, props),
});
}
```
#### Usage [Section titled “Usage”](#usage)
```tsx
import { Button } from "./components/button";
// Renders:
//
// Contact
//
export default () => }>Contact;
```
# Skills
> Use the cva-migrate Agent Skill to upgrade a project between cva versions.
## `cva-migrate` [Section titled “cva-migrate”](#cva-migrate) [`cva-migrate`](https://github.com/joe-bell/cva/tree/main/skills/cva-migrate) upgrades a project between `cva` versions. Install it for repeated use, or paste the prompt into your agent to run it once: * Command
```sh
npx skills add https://github.com/joe-bell/cva --skill cva-migrate
```
* Prompt
```text
Run `npx skills use "https://github.com/joe-bell/cva" --skill "cva-migrate"` and follow the generated skill instructions now. Read its complete output, redirecting it to a temporary file first if necessary. Resolve relative paths from the supporting-files directory it provides.
```
Then tell the agent which version to install: > Migrate this project to the upcoming `cva@1.0.0-beta.12`. The two curated routes upgrade `cva@1.0.0-beta.0` through `beta.10` to `beta.11`, and `beta.0` through `beta.11` to the upcoming `beta.12`. For other routes, the skill checks every relevant [GitHub release](https://github.com/joe-bell/cva/releases), the tagged package metadata, and the published npm artifacts. It stops if those sources do not support a migration step.
# Tools
> Read a component's variant names, values, and defaults with getSchema from cva/tools.
The `cva/tools` entry point holds the helpers that read a component’s variants back out at runtime. It’s a separate entry point, so an ESM bundler drops it from a build that never imports it. ## `getSchema` [Section titled “getSchema”](#getschema) Re-declaring variants for a Storybook story or prop table creates a second copy to keep in sync by hand. `getSchema` reads them from the component instead, so you declare each variant once:
```ts
import { cva } from "cva";
import { getSchema } from "cva/tools";
const button = cva({
base: "button",
variants: {
intent: { primary: "button--primary", secondary: "button--secondary" },
size: { small: "button--small", large: "button--large" },
},
defaultVariants: { intent: "primary", size: "small" },
});
getSchema(button);
// => {
// intent: { values: ["primary", "secondary"], defaultValue: "primary" },
// size: { values: ["small", "large"], defaultValue: "small" },
// }
```
The schema is fully typed: `values` narrows to the variant’s literal values, and `defaultValue` only appears when the component declares one. Hover the result in your editor and you see the same shape the runtime returns:
```ts
import { cva } from "cva";
import { getSchema } from "cva/tools";
const badge = cva({
base: "badge",
variants: {
tone: { info: "badge--info", warning: "badge--warning" },
round: { true: "badge--round", false: "badge--square" },
weight: { 400: "badge--regular", 700: "badge--bold" },
},
defaultVariants: { tone: "info" },
});
const schema = getSchema(badge);
// ^ {
// tone: {
// values: readonly ("info" | "warning")[];
// defaultValue: "info";
// };
// round: { values: readonly boolean[] };
// weight: { values: readonly (400 | 700)[] };
// }
```
Boolean and numeric variant keys come back as booleans and numbers, matching the props the component accepts rather than the object keys they were written as. [Internal variants](/beta/getting-started/variants#internal-variants) are omitted. For the full signature, see the [API reference](/beta/api-reference#getschema).
# TypeScript
> Extract variant types with VariantProps and require specific variants with TypeScript utility types.
## Extracting variant types [Section titled “Extracting variant types”](#extracting-variant-types) `cva` offers the `VariantProps` helper to extract variant types. `VariantProps` omits variant names prefixed with `_`; see [Internal variants](/beta/getting-started/variants#internal-variants). components/button.ts
```ts
import type { VariantProps } from "cva";
import { cva, cx } from "cva";
/**
* Button
*/
export type ButtonProps = VariantProps;
export const button = cva(/* … */);
```
## Required variants [Section titled “Required variants”](#required-variants) To keep the API small and unopinionated, `cva` **doesn’t** offer a built-in solution for setting required variants. Instead, we recommend using TypeScript’s [Utility Types](https://www.typescriptlang.org/docs/handbook/utility-types.html): components/button.ts
```ts
import { cva, type VariantProps } from "cva";
export type ButtonVariantProps = VariantProps;
export const buttonVariants = cva({
base: "…",
variants: {
optional: { a: "…", b: "…" },
required: { a: "…", b: "…" },
},
});
/**
* Button
*/
export interface ButtonProps
extends
Omit,
Required> {}
export const button = (props: ButtonProps) => buttonVariants(props);
// ❌ TypeScript Error:
// Argument of type "{}": is not assignable to parameter of type "ButtonProps".
// Property "required" is missing in type "{}" but required in type
// "ButtonProps".
button({});
// ✅
button({ required: "a" });
```
# Variants
> Create variants, compound variants, and disable variants with cva@beta.
## Creating variants [Section titled “Creating variants”](#creating-variants) To kick things off, let’s build a “basic” `button` component, using `cva` to handle our variant’s classes components/button.ts
```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"
```
## Internal variants [Section titled “Internal variants”](#internal-variants) 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`](/beta/getting-started/typescript#extracting-variant-types) and [`getSchema`](/beta/api-reference#getschema) omit them.
```ts
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;
// => { 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:
```tsx
interface Props
extends
React.ButtonHTMLAttributes,
VariantProps {
active?: boolean;
}
// `_intent` never appears in `Props`, so a consumer can't set it.
function Button({ active, size, className, ...props }: Props) {
return (
);
}
```
A composed-only internal variant is omitted from the composer’s `VariantProps` too. You can override its inherited default directly from the composer without redeclaring the variant (see [Extending variants](/beta/getting-started/composing-components#extending-variants)).
```ts
const base = cva({
variants: {
_tone: { quiet: "tone-quiet", loud: "tone-loud" },
},
defaultVariants: { _tone: "quiet" },
});
const card = cva({
composes: base,
defaultVariants: { _tone: "loud" },
});
card();
// => "tone-loud"
```
## Compound variants [Section titled “Compound variants”](#compound-variants) Variants that apply when multiple other variant conditions are met. components/button.ts
```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: "…",
},
],
});
```
### Targeting multiple variant conditions [Section titled “Targeting multiple variant conditions”](#targeting-multiple-variant-conditions) components/button.ts
```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: "…",
},
],
});
```
## Disabling variants [Section titled “Disabling variants”](#disabling-variants) 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](https://developer.mozilla.org/en-US/docs/Web/CSS/unset)).
```ts
import { cva } from "cva";
const button = cva({
base: "button",
variants: {
intent: {
unset: null,
primary: "button--primary",
secondary: "button--secondary",
},
},
});
button({ intent: "unset" });
// => "button"
```
# What's New?
> What changed in cva@1.0, defineConfig, composes, the cva/tools entry point, and other breaking changes since class-variance-authority@0.x.
What’s changed since `class-variance-authority@0.*`? ## Migrate [Section titled “Migrate”](#migrate) If you’re already using a `cva` beta, [run the `cva-migrate` Agent Skill](/beta/getting-started/skills) to upgrade between beta versions. The rest of this page covers migrating from `class-variance-authority@0.x`. ## Requirements [Section titled “Requirements”](#requirements) TypeScript projects using `cva@beta` need TypeScript 6.0 or later. JavaScript usage is unaffected. ## Features [Section titled “Features”](#features) ### 1. `defineConfig` [Section titled “1. defineConfig”](#1-defineconfig) Roll your own `cva` via the new [`defineConfig` API in `cva/config`](/beta/api-reference#cvaconfig). Use `cva`/`cx` with `cn`, `tailwind-merge`, or your own concatenator via the [`cx` option](/beta/getting-started/installation#merging-classes). ### 2. `composes` [Section titled “2. composes”](#2-composes) Shallow merge one or more `cva` components into a single component via the new [`composes` property](/beta/getting-started/composing-components). This [replaces the `compose` function](#1-compose--composes). ### 3. `getSchema` [Section titled “3. getSchema”](#3-getschema) Extract a plain-object schema (variant names, values and defaults) from a `cva` component via the new [`cva/tools`](/beta/getting-started/tools) entry point’s `getSchema`. Use it to generate Storybook controls or any other UI that reads a component’s variants. ### 4. Internal variants [Section titled “4. Internal variants”](#4-internal-variants) A variant name prefixed with `_` is now treated as [internal](/beta/getting-started/variants#internal-variants). It stays off a wrapping component’s public props because `VariantProps` and `getSchema` omit it, while the `cva` component still accepts it. Breaking change If a component already declared a variant with a leading underscore (`_state`, `_internal`, etc.), that variant now disappears from `VariantProps` and from `getSchema` results. The component still accepts it directly, but you’ll need to rename it if you exposed it as a public prop. ### 5. Bring your own concatenator with `cva/config` [Section titled “5. Bring your own concatenator with cva/config”](#5-bring-your-own-concatenator-with-cvaconfig) The `cva` package is a preset over the core: it wires `cva` and `cx` to `clsx`, like `class-variance-authority`. Use `cva/config` with a required `cx` option to configure `clsx/lite`, `cn`, `tailwind-merge`, or your own concatenator. ## Deprecations [Section titled “Deprecations”](#deprecations) ### 1. `compose` → `composes` [Section titled “1. compose → composes”](#1-compose--composes) The `compose` method was deprecated in favor of the `composes` property inside `cva`, and `cva@1.0` removes it: there is no `compose` to import. Pass a single component directly, or pass multiple components as an array.
```diff
-import { cva, compose } from "cva";
+import { cva } from "cva";
const box = cva({ /* ... */ });
const root = cva({ /* ... */ });
-const card = compose(box, root);
+const card = cva({ composes: [box, root] });
```
See [Composing Components](/beta/getting-started/composing-components) for more details and migration examples. ## Enhancements [Section titled “Enhancements”](#enhancements) ### 1. `class-variance-authority` → `cva` [Section titled “1. class-variance-authority → cva”](#1-class-variance-authority--cva) One of the biggest (and let’s be honest, most important) complaints about `class-variance-authority` was that the name was just too damn long. Shout-out to GitHub for transferring `npm` ownership of `cva`! ### 2. `cva` now accepts a single parameter [Section titled “2. cva now accepts a single parameter”](#2-cva-now-accepts-a-single-parameter) Base styles are now applied via the named `base` property.
```diff
-import { cva } from "class-variance-authority";
+import { cva } from "cva";
const component = cva({ base: "your-base-class" });
```
### 3. Goodbye `null` [Section titled “3. Goodbye null”](#3-goodbye-null) Previously, passing `null` to a variant would disable it completely, to match the behavior of Stitches.js. However, this [caused a great deal of confusion](https://github.com/joe-bell/cva/discussions/97). Instead, we now recommend explicitly [rolling your own `unset` variant](/beta/getting-started/variants#disabling-variants). ### 4. Clearer type guards [Section titled “4. Clearer type guards”](#4-clearer-type-guards) `cva` uses generic type parameters to infer variant types. Some users mistook these for a customization option. If you now pass a generic type parameter, `cva` throws an error. ### 5. Faster component calls [Section titled “5. Faster component calls”](#5-faster-component-calls) Calling a `cva` component is up to 1000% faster than `0.7`. The runtime works out as much of the class list as it can when you create the component, then calls your concatenator once per call, instead of copying props and spreading arrays along the way: * Components with variants and compound variants: 300% to 550% faster per call. * Matching a dozen compound variants: 700% to 1000% faster. * One component called with 24 different prop shapes: 400% faster. * Components with only a `base`: 50% faster. The `cva` entry is now 1.62 kB (brotli), up from 1.4 kB. The precomputed tables are the difference. Thanks to [@fveracoechea](https://github.com/fveracoechea) for inspiring this work! See [the pull request](https://github.com/joe-bell/cva/pull/419) for the full numbers. ### 6. Configuration is read when you create the component [Section titled “6. Configuration is read when you create the component”](#6-configuration-is-read-when-you-create-the-component) `cva` used to re-read your config on every call, so mutating it afterwards quietly changed the output. It now reads `base`, `variants`, `compoundVariants`, `defaultVariants` and `composes` during creation. A call reads only the props you pass it, plus the `cx` from your `defineConfig` options, which stays live. That is where most of the speed above comes from. Breaking change Treat a config, and everything it references, as immutable once you’ve created the component. Mutating it afterwards is unsupported, and how much of the change shows up is undefined. Create a new component instead:
```diff
-const config = {
base: "button",
variants: { intent: { primary: "bg-blue-500" } },
-};
-const button = cva(config);
-config.variants.intent.primary = "bg-indigo-500";
+const button = cva({
base: "button",
variants: { intent: { primary: "bg-blue-500" } },
+});
+const indigoButton = cva({
base: "button",
variants: { intent: { primary: "bg-indigo-500" } },
+});
```
Composition follows the same rule: a component pushed onto a `composes` array after the fact won’t show up, and editing a composed child’s own config won’t update the parent’s merged variants. Recreate the parent. `component.config` still exposes the merged `variants` and `defaultVariants` as fresh objects, but it’s internal: reading it is fine, mutating it is unsupported and may partly show up in later output. Frozen configs work, and `cva` never mutates or freezes anything you pass it. ### 7. Faster type checking [Section titled “7. Faster type checking”](#7-faster-type-checking) The authoring types were reworked so TypeScript does less work to reach the same answers. Your inferred prop types, `getSchema` results and emitted declarations are identical. Measured with `tsc --extendedDiagnostics` against the previous beta: * A project with 200 components type-checks about 10% faster, with 26% fewer type instantiations. * Composed components: 16% fewer instantiations. Components with a `getSchema` call: 24% fewer. See [the pull request](https://github.com/joe-bell/cva/pull/420) for the full numbers.
# Tutorials
> Community videos, podcasts, and articles about cva.
## YouTube [Section titled “YouTube”](#youtube) *  # [Class Variance Authority (CVA) Quickstart](https://www.youtube.com/watch?v=kHQNK2jU_TQ) Coding in Public•15th September 2023 *  # [Authoring Components with CVA + tailwindcss](https://www.youtube.com/watch?v=qGQRdCg6JRQ) React Tips with Brooks Lybrand•29th August 2023 *  # [Creating High-Quality React Components: Best Practices for Reusability](https://www.youtube.com/watch?v=eXRlVpw1SIQ) Josh tried coding•26th February 2023 *  # [Building a design system in Next.js with Tailwind!](https://www.youtube.com/watch?v=kTSwLLFa3WM) mewtru•4th January 2023 *  # [Large Tailwind Components — What to do About All Those Classes](https://www.youtube.com/watch?v=B6FrDu2Qbt0) frontendfyi•21st November 2022 *  # [Tru Narla: Building a design system in Next.js with Tailwind](https://www.youtube.com/watch?v=T-Zv73yZ_QI) mewtru•8th October 2022 ## Audio [Section titled “Audio”](#audio) * [JS Party – Episode #277](https://changelog.com/jsparty/277) (25th May 2023) ## Articles [Section titled “Articles”](#articles) * ["Building a design system using Solidjs, Typescript, SCSS, CSS Variables and Vite"](https://dev.to/yaldram/building-a-design-system-using-solidjs-typescript-scss-css-variables-and-vite-setup-45nj) by Arsalan Ahmed Yaldram(15th April 2023) * ["Building a design system with dark mode using React, Typescript, scss, cva and Vite"](https://dev.to/yaldram/build-a-design-system-with-dark-mode-using-react-typescript-scss-cva-and-vite-setup-5778) by Arsalan Ahmed Yaldram(20th March 2023)