>
);
```
* 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"
```
Keep composition arrays immutable and typed
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”
e.g. `` instead of ``
```tsx
import * as Accordion from "./Accordion";
function Example() {
return (
Section 1Content 1
);
}
```
`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"
```
Extra classes may not override
Extra classes are appended to the generated string. With the default `cx`, their position does not make them override conflicting CSS rules. For Tailwind CSS overrides, choose a [class conflict strategy](/beta/getting-started/installation#handling-class-conflicts).
# Installation
> Install cva@beta, configure Tailwind CSS IntelliSense, and handle utility style conflicts.
* 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
```
## Join conditional classes
[Section titled “Join conditional classes”](#join-conditional-classes)
Use the included cx
`cva` includes `cx` for joining strings, arrays, and conditional objects. You don’t need a separate `clsx` or `classnames` dependency for this.
```ts
import { cx } from "cva";
const active = true;
cx("button", ["rounded", { "button--active": active }]);
// => "button rounded button--active"
```
`cx` joins class names; it doesn’t resolve conflicting CSS rules. See the [`cx` reference](/beta/api-reference#cx) for its inputs and [handling class conflicts](#handling-class-conflicts) for Tailwind CSS options.
## Tailwind CSS
[Section titled “Tailwind CSS”](#tailwind-css)
Write complete Tailwind class names
Tailwind CSS scans source text and cannot detect interpolated names such as `bg-${tone}-500`. Map each variant value to a full class string instead.
```ts
import { cva } from "cva";
const badge = cva({
variants: {
tone: {
info: "bg-blue-500 text-white",
warning: "bg-amber-500 text-black",
},
},
});
```
See Tailwind’s [class detection guide](https://tailwindcss.com/docs/detecting-classes-in-source-files#dynamic-class-names) for how source scanning works. The editor settings below add autocompletion inside `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"]
}
```
## Handling class conflicts
[Section titled “Handling class conflicts”](#handling-class-conflicts)
By default, `cva` joins classes with [`clsx`](https://github.com/lukeed/clsx). It does not resolve conflicting Tailwind CSS utilities. Choose one of the approaches below:
### 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 });
```
### cva/tailwindcss
[Section titled “cva/tailwindcss”](#cvatailwindcss)
`cva/tailwindcss` is a set of custom Tailwind CSS variants, for use with or without `cva`. Import it after Tailwind CSS:
```css
@import "tailwindcss";
@import "cva/tailwindcss";
```
#### base:
[Section titled “base:”](#base)
The `base:` variant marks a utility as a component default, so any ordinary utility overrides it through the CSS cascade instead of runtime class merging.
Prefix overridable component defaults with `base:`, including defaults selected by variants or compound variants. Write state styles as ordinary utilities:
```ts
import { cva } from "cva";
export const button = cva({
base: [
"base:bg-blue-600 base:px-4 base:py-2 base:text-white",
"hover:bg-blue-700",
],
});
button({ 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](https://x.com/diegohaz/status/1897776774206853269) and [a related `:where()` example](https://x.com/acd02/status/1897943176863756335).
#### Limitations
[Section titled “Limitations”](#limitations)
`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!`.
For runtime conflict resolution, [`cn`](#cn) and [`tailwind-merge`](#tailwind-merge) remove earlier conflicting utilities from the class string.
### tailwind-merge
[Section titled “tailwind-merge”](#tailwind-merge)
Install [`tailwind-merge`](https://github.com/dcastil/tailwind-merge) and combine it with the `cx` already exported by `cva`. This preserves strings, arrays, and conditional objects while resolving Tailwind CSS conflicts:
```sh
pnpm add tailwind-merge
```
cva.config.ts
```ts
import { defineConfig } from "cva/config";
import { cx as joinClasses } from "cva";
import { twMerge } from "tailwind-merge";
export const { cva, cx: cn } = defineConfig({
cx: (...inputs) => twMerge(joinClasses(...inputs)),
});
```
Bare `twMerge`
Use bare `twMerge` when your authored values are strings or arrays. TypeScript rejects object syntax, but JavaScript has no compile-time protection. Exported configurations can expose tailwind-merge’s `ClassNameValue` in your library declarations.
```ts
import { defineConfig } from "cva/config";
import { twMerge } from "tailwind-merge";
export const { cva, cx: cn } = defineConfig({ cx: twMerge });
```
### 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 Agent Skills to build cva components and migrate between cva versions.
Agent Skills give your coding agent guidance for component work and version upgrades.
## `cva-best-practices`
[Section titled “cva-best-practices”](#cva-best-practices)
[`cva-best-practices`](https://github.com/joe-bell/cva/tree/main/skills/cva-best-practices) condenses these docs into guidance for typed variants, defaults, composition, class conflicts, and schema-driven galleries. It checks the installed package before applying beta APIs.
Install it for component work:
```sh
npx skills add https://github.com/joe-bell/cva --skill cva-best-practices
```
Then give your agent a component task:
> Use cva-best-practices to add size and intent variants to this button, keeping its native HTML props.
The relevant guides remain the source of truth for each recommendation. Use the migration skill below when changing 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 `cva@1.0.0-beta.12`.
The three curated routes upgrade `class-variance-authority@0.6.0` through `0.7.1` to `cva@1.0.0-beta.12`, `cva@1.0.0-beta.0` through `beta.10` to `beta.11`, and `beta.0` through `beta.11` to `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).
## Generate a React variant gallery
[Section titled “Generate a React variant gallery”](#generate-a-react-variant-gallery)
The runnable [React with Tailwind CSS example](/beta/examples/react/tailwindcss#basic-component) exports its `button` class function separately from its React wrapper. The wrapper supplies `intent`, `size`, and `disabled` defaults so styling and native attributes use the same values:
examples/beta/react-with-tailwindcss/src/components/button/button.tsx
```tsx
import React from "react";
import { cva, type VariantProps } from "cva";
export const button = cva({
base: "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: "py-1 px-2 text-sm",
medium: "py-2 px-4 text-base",
},
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" },
],
});
export interface ButtonProps
extends
Omit, "disabled">,
VariantProps {}
export const Button: React.FC = ({
className,
intent = "primary",
size = "medium",
disabled = false,
...props
}) => (
);
```
Pass `button`, the `cva` class function, to `getSchema`. The React `Button` renders markup and is not a valid input. Read the schema outside the render function, then map its typed values into buttons:
examples/beta/react-with-tailwindcss/src/button-gallery.tsx
```tsx
import React from "react";
import { cx } from "cva";
import { getSchema } from "cva/tools";
import { Button, button } from "./components";
const schema = getSchema(button);
const intents = [undefined, ...schema.intent.values];
const sizes = [undefined, ...schema.size.values];
const isDisabled = schema.disabled.values;
export function ButtonGallery() {
return (
);
}
```
Adding an intent or size to `button` now adds it to the gallery without another list to update. The default cells leave those props unset, exercising the React wrapper’s prop defaults, while each row passes `disabled` explicitly. `getSchema` cannot discover wrapper defaults, so use `defaultVariants` when the class function itself should expose a `defaultValue`.
The [React examples](/beta/examples/react/tailwindcss) use this approach with default, size, intent, and disabled states.
Keep schema inspection in stories
Read a schema outside rendering and derive layout counts from its `values` arrays. Keep this inspection in stories or documentation when the application does not need it at runtime.
# 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.
Keep component props inferred
Use `VariantProps` instead of repeating variant unions. Combine it with native element props when wrapping a UI component, and forward semantic attributes such as `disabled` to the element as well as the class function.
`VariantProps` contains public variant props, not `class` or `className`. It omits variant names prefixed with `_`; see [Internal variants](/beta/getting-started/variants#internal-variants).
components/button.ts
```ts
import { cva, type VariantProps } from "cva";
export const button = cva({
base: "button",
variants: {
intent: { primary: "button--primary", secondary: "button--secondary" },
},
});
export type ButtonProps = VariantProps;
```
For a React component that combines these variants with native button props, see [generating a React variant gallery](/beta/getting-started/tools#generate-a-react-variant-gallery).
## 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)
Note
Although `cva` is a tiny (1.64 KB compressed) library, it’s best to use in an environment with server-side rendering (SSR) or static-site generation (SSG): your user probably doesn’t need this JavaScript, especially for static components.
To kick things off, let’s build a “basic” `button` component, using `cva` to handle our variant’s classes
Note
Use of Tailwind CSS is optional
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"
```
## Default variants
[Section titled “Default variants”](#default-variants)
`defaultVariants` supplies defaults when calling the class function. Omitting a prop, or passing `undefined`, uses its default.
Prefer framework prop defaults in UI components
For framework components (e.g. React or Vue), prefer your framework’s prop defaults so the same value is available to both styling and markup. For example, defaulting `disabled` in `defaultVariants` changes class selection but does not set the HTML button’s `disabled` attribute. Set the prop default in the wrapper and pass that value to both the class function and the element.
Use `defaultVariants` when the class function itself should supply defaults to its callers. [`getSchema`](/beta/getting-started/tools) reads those defaults, but cannot discover defaults declared in framework wrappers. Avoid maintaining the same default in both places.
To select no classes for a variant, declare an explicit option such as `unset: null`; see [disabling variants](#disabling-variants).
## 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 since class-variance-authority@0.x, covering cva/config, cva/tailwindcss, composes, getSchema, internal variants, and breaking changes.
What’s changed since `class-variance-authority@0.*`?
## Requirements
[Section titled “Requirements”](#requirements)
* [Install `cva@beta`](/beta/getting-started/installation).
* TypeScript 6.0 or later for TypeScript projects.
* Tailwind CSS v4 for the optional `cva/tailwindcss` stylesheet.
## Features
[Section titled “Features”](#features)
### 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. Bring your own concatenator with `cva/config`
[Section titled “2. Bring your own concatenator with cva/config”](#2-bring-your-own-concatenator-with-cvaconfig)
`cva` and `cx` still use `clsx` by default. For your own setup, use [`defineConfig` from `cva/config`](/beta/api-reference#cvaconfig). Its required `cx` option accepts [`cn` or `tailwind-merge`](/beta/getting-started/installation#handling-class-conflicts), [`clsx/lite` for string-only classes](/beta/api-reference#advanced-concatenators), or your own concatenator.
#### `shadcn`
[Section titled “shadcn”](#shadcn)
The [`shadcn` CLI](https://ui.shadcn.com/docs/registry/github) installs `cva@beta` with [`cn`](/beta/getting-started/installation#cn) and creates `cva.config.ts` in your project root:
```sh
pnpm dlx shadcn@latest add joe-bell/cva/cn
```
#### `cva/tailwindcss`
[Section titled “cva/tailwindcss”](#cvatailwindcss)
Import [`cva/tailwindcss`](/beta/getting-started/installation#cvatailwindcss) in a Tailwind CSS v4 project and prefix component defaults with `base:`. Ordinary utilities override them through the CSS cascade, with no runtime class merging.
### 3. Performance
[Section titled “3. Performance”](#3-performance)
Component calls are up to **1000%** faster than `class-variance-authority@0.7`. `cva` prepares class lookups when you create the component and runs your concatenator once per call:
* 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 **1.64 KB** (brotli), measured from the current build.
Thanks to [@fveracoechea](https://github.com/fveracoechea) for inspiring this work! See the [runtime benchmarks](https://github.com/joe-bell/cva/pull/419) for the full results.
#### TypeScript
[Section titled “TypeScript”](#typescript)
TypeScript does less work without changing inferred props, `getSchema` results, or emitted declarations. The [type-checking benchmarks](https://github.com/joe-bell/cva/pull/420) compare against the preceding beta revision using `tsc --extendedDiagnostics`:
* 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.
### 4. Internal variants
[Section titled “4. Internal variants”](#4-internal-variants)
Prefix a variant name with `_` to make it [internal](/beta/getting-started/variants#internal-variants). `VariantProps` and `getSchema` omit it, but you can still pass it directly to the `cva` component.
Breaking change
Rename any existing `_`-prefixed variant you expose as a public prop, or it will disappear from `VariantProps`.
### 5. `composes`
[Section titled “5. composes”](#5-composes)
Shallow merge one or more `cva` components with the [`composes` property](/beta/getting-started/composing-components).
This [replaces the `compose` function](#6-compose--composes) from earlier betas.
### 6. `getSchema`
[Section titled “6. getSchema”](#6-getschema)
[`getSchema`](/beta/getting-started/tools#getschema) from `cva/tools` returns a plain object of variant names, values, and defaults. Use it to generate Storybook controls or variant galleries from your component.
### 7. Agent Skills
[Section titled “7. Agent Skills”](#7-agent-skills)
Two Agent Skills help your coding agent work with `cva`:
* [`cva-best-practices`](/beta/getting-started/skills#cva-best-practices) guides component authoring.
* [`cva-migrate`](/beta/getting-started/skills#cva-migrate) handles version upgrades.
### 8. Tailwind CSS IntelliSense
[Section titled “8. Tailwind CSS IntelliSense”](#8-tailwind-css-intellisense)
The [Tailwind CSS IntelliSense setup](/beta/getting-started/installation#intellisense) now uses `classFunctions` instead of `classRegex`. It covers Visual Studio Code, Zed, Neovim, and WebStorm, and works with `class-variance-authority` too.
## Breaking changes
[Section titled “Breaking changes”](#breaking-changes)
### 1. `cva` now accepts a single parameter
[Section titled “1. cva now accepts a single parameter”](#1-cva-now-accepts-a-single-parameter)
Pass base styles in the config object’s `base` property:
```diff
-import { cva } from "class-variance-authority";
+import { cva } from "cva";
-const component = cva("your-base-class");
+const component = cva({ base: "your-base-class" });
```
The config object is required. Replace `cva()` with `cva({})` to avoid a TypeScript error.
### 2. Goodbye `null`
[Section titled “2. Goodbye null”](#2-goodbye-null)
Passing `null` used to disable a variant, matching Stitches.js. This [caused a great deal of confusion](https://github.com/joe-bell/cva/discussions/97).
TypeScript now rejects `null` in props, `defaultVariants`, and `compoundVariants` selectors. At runtime, a `null` prop applies the variant’s default class instead of disabling it. Compound variants still compare the `null` as-is, so it isn’t interchangeable with `undefined`.
Use an explicit [`unset` variant](/beta/getting-started/variants#disabling-variants) instead:
```diff
const button = cva({
base: "button",
variants: { size: { small: "button--small" } },
variants: { size: { unset: null, small: "button--small" } },
defaultVariants: { size: "small" },
});
-button({ size: null });
+button({ size: "unset" });
+// => "button"
```
### 3. Clearer type guards
[Section titled “3. Clearer type guards”](#3-clearer-type-guards)
`cva` infers variant types from your config. Passing a generic type argument now produces a TypeScript error.
### 4. Type imports move to the root
[Section titled “4. Type imports move to the root”](#4-type-imports-move-to-the-root)
`cva` has no `types` entry point. Import `ClassValue`, `ClassArray`, and `ClassDictionary` from `cva` itself, alongside `VariantProps`.
Breaking change
* `class-variance-authority/types` has no equivalent. `ClassProp`, `ClassPropKey`, `OmitUndefined`, and `StringToBoolean` are no longer exported.
* `CxOptions` and `CxReturn` are now `CXOptions` and `CXReturn`.
* A variant named `__proto__` is rejected by the types.
```diff
-import type { ClassValue } from "class-variance-authority/types";
-import type { CxOptions, VariantProps } from "class-variance-authority";
+import type { ClassValue, CXOptions, VariantProps } from "cva";
```
### 5. Configuration is read when you create the component
[Section titled “5. Configuration is read when you create the component”](#5-configuration-is-read-when-you-create-the-component)
`cva` now reads `base`, `variants`, `compoundVariants`, `defaultVariants`, and `composes` when you create the component, which accounts for most of the [runtime speed improvement](#3-performance). Calls read your props and the current `cx` from `defineConfig`.
Breaking change
Treat the config and its referenced values as immutable after creation. Later mutations are unsupported and may only partly affect the output. 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" } },
+});
```
Recreate the parent after adding to a `composes` array or changing a child’s config. Neither change updates the parent’s merged variants.
`component.config` exposes fresh objects for the merged `variants` and `defaultVariants`. This internal property is safe to read, but mutating it is unsupported and may affect later output. Frozen configs work, and `cva` never mutates or freezes your inputs.
### 6. `compose` → `composes`
[Section titled “6. compose → composes”](#6-compose--composes)
This change only affects earlier `cva` betas. `class-variance-authority` never had `compose`.
`cva@1.0` removes the deprecated `compose` function. Use `composes` inside `cva` instead, passing a single component or 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] });
```
Note
Conflicting `defaultVariants` now use the [last declared default for every component](/beta/getting-started/composing-components#extending-variants). `compose` let each component resolve its own default.
## Migrating to 1.0
[Section titled “Migrating to 1.0”](#migrating-to-10)
Use the [`cva-migrate` Agent Skill](/beta/getting-started/skills#cva-migrate) to upgrade from `class-variance-authority@0.x` or an earlier `cva` beta.
# 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)
Created a tutorial?
Add your content to the [tutorials directory](https://github.com/joe-bell/cva/tree/main/docs/src/content/tutorials).
# 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
[Section titled “Tailwind”](#tailwind)
button.11ty.js
```js
const { cva } = require("class-variance-authority");
// ⚠️ Disclaimer: Use of Tailwind CSS is optional
const button = cva("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: ["text-sm", "py-1", "px-2"],
medium: ["text-base", "py-2", "px-4"],
},
},
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/latest/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/latest/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 "class-variance-authority";
const button = cva("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 "class-variance-authority";
const greeter = cva("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/latest/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/latest/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.
This example builds a `button` component with `cva` and Tailwind CSS in a React project.
[View source on GitHub ↗](https://github.com/joe-bell/cva/tree/main/examples/latest/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/latest/react-with-tailwindcss?file=src/components/button/button.tsx) 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/latest/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/latest/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/latest/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/latest/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
## 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.
Example: With Tailwind
```tsx
export const Example = () => (
<>
>
);
```
* 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
> Extend and concatenate cva components with cx.
Tip
`cva@beta` has first-class composition via the `composes` property. See [Composing components](/beta/getting-started/composing-components).
While `cva` doesn’t offer a built-in method for composing components together, it does offer the tools to *extend* components on your own terms…
For example; two `cva` components, concatenated together with `cx`:
components/card.ts
```ts
import type { VariantProps } from "class-variance-authority";
import { cva, cx } from "class-variance-authority";
/**
* Box
*/
export type BoxProps = VariantProps;
export const box = cva(["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,
},
});
/**
* Card
*/
type CardBaseProps = VariantProps;
const cardBase = cva(["card", "border-solid", "border-slate-300", "rounded"], {
variants: {
shadow: {
md: "drop-shadow-md",
lg: "drop-shadow-lg",
xl: "drop-shadow-xl",
},
},
});
export interface CardProps extends BoxProps, CardBaseProps {}
export const card = ({ margin, padding, shadow }: CardProps = {}) =>
cx(box({ margin, padding }), cardBase({ shadow }));
```
# 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 "class-variance-authority";
const button = cva("font-semibold");
button({ class: "m-4" });
// => "font-semibold m-4"
button({ className: "m-4" });
// => "font-semibold m-4"
```
# Installation
> Install class-variance-authority and configure Tailwind CSS IntelliSense and style-conflict handling.
* pnpm
```sh
pnpm i class-variance-authority
```
* npm
```sh
npm i class-variance-authority
```
* yarn
```sh
yarn add class-variance-authority
```
* bun
```sh
bun add class-variance-authority
```
* deno
```sh
deno add 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”](https://www.npmjs.com/package/cva).
On 2022/02/16, GitHub transferred NPM ownership of `cva` to [Joe Bell](https://joebell.studio). 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`](https://docs.npmjs.com/cli/v6/commands/npm-install)
```sh
npm i cva@npm:class-variance-authority
```
2. Then import like so:
```ts
import { cva } from "cva";
// …
```
## 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 [`settings.json`](https://code.visualstudio.com/docs/getstarted/settings):
```json
{
"tailwindCSS.classFunctions": ["cva", "cx"]
}
```
* Zed
Add the following to your [`.zed/settings.json`](https://zed.dev/docs/configuring-zed#settings-files):
```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"]
}
```
### Handling style conflicts
[Section titled “Handling style conflicts”](#handling-style-conflicts)
By default, `cva` joins class names but does not resolve conflicting Tailwind CSS utilities. Choose one of the approaches below:
#### CSS defaults with `base:`
[Section titled “CSS defaults with base:”](#css-defaults-with-base)
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:
```css
@import "tailwindcss";
@custom-variant base {
@layer base {
:where(&) {
@slot;
}
}
}
```
Prefix your defaults with `base:` and write state styles as ordinary utilities:
```ts
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](https://x.com/diegohaz/status/1897776774206853269) and [a related `:where()` example](https://x.com/acd02/status/1897943176863756335).
`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!`.
#### tailwind-merge
[Section titled “tailwind-merge”](#tailwind-merge)
If you’d rather not think about the cascade, [`tailwind-merge`](https://github.com/dcastil/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
```ts
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 {}
export const button = (variants: ButtonVariants) =>
twMerge(buttonVariants(variants));
```
# 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
components/button.ts
```ts
import type { VariantProps } from "class-variance-authority";
import { cva, cx } from "class-variance-authority";
/**
* 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 "class-variance-authority";
export type ButtonVariantProps = VariantProps;
export const buttonVariants = cva("…", {
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 default variants with cva.
## Creating variants
[Section titled “Creating variants”](#creating-variants)
Note
Although `cva` is a tiny (596 B compressed) library, it’s best to use in an environment with server-side rendering (SSR) or static-site generation (SSG): your user probably doesn’t need this JavaScript, especially for static components.
To kick things off, let’s build a “basic” `button` component, using `cva` to handle our variant’s classes
Note
Use of Tailwind CSS is optional
components/button.ts
```ts
import { cva } from "class-variance-authority";
const button = cva(["font-semibold", "border", "rounded"], {
variants: {
intent: {
primary: ["bg-blue-500", "text-white", "border-transparent"],
// **or**
// primary: "bg-blue-500 text-white border-transparent hover:bg-blue-600",
secondary: ["bg-white", "text-gray-800", "border-gray-400"],
},
size: {
small: ["text-sm", "py-1", "px-2"],
medium: ["text-base", "py-2", "px-4"],
},
// `boolean` variants are also supported!
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",
// **or** if you're a React.js user, `className` may feel more consistent:
// className: "uppercase"
class: "uppercase",
},
],
defaultVariants: {
intent: "primary",
size: "medium",
disabled: false,
},
});
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"
```
## 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 "class-variance-authority";
const button = cva("…", {
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 "class-variance-authority";
const button = cva("…", {
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: "…",
},
],
});
```
# 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)
Created a tutorial?
Add your content to the [tutorials directory](https://github.com/joe-bell/cva/tree/main/docs/src/content/tutorials).