class-variance-authority review
class-variance-authority 0.7.1 turns a finite set of component choices into a function that returns one class string. You define names such as `tone`, `scale`, and `disabled`, map their allowed values to classes, and use `VariantProps` to derive the matching TypeScript props. It has no React or Tailwind peer, so the same selector can sit behind any view layer. Our browser build came to 1.1 KB minified and 0.6 KB gzipped. The current stable release adds license comments and changes the `clsx` dependency from a fixed version to `^2.1.1`; it does not add a new styling feature. The newer `cva` package is a separate 1.0 beta, not the npm `latest` version covered here.
Our install of class-variance-authority 0.7.1 finished in 0.3 seconds, used 1 MB across 2 packages, added 0.6 KB gzipped, and produced 0 audit findings. It is worth installing when typed variant tables replace repeated component branches, provided your project already has a plan for Tailwind conflicts and multi-slot components.
We installed it
| Install | ✓ · 0.3s | 2 packages on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package with exports map |
| Browser | 0.6 KB | gzipped (1.1 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does class-variance-authority install cleanly?
Yes. In a fresh container with an empty cache, npm install class-variance-authority finished in 0.3s, leaving 2 packages and 1 MB on disk. npm audit reported no known vulnerabilities.
How much does class-variance-authority add to a browser bundle?
0.6 KB gzipped (1.1 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does class-variance-authority work with both ESM and CommonJS?
Yes. Both import 'class-variance-authority' and require('class-variance-authority') worked in Node 22 in our run. The package is published as CommonJS with an exports map.
Does class-variance-authority include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
class-variance-authority or clsx: which should you use?
clsx: Use it when you only need conditional class joining and do not need variant prop types or compound rules. Our install of class-variance-authority 0.7.1 finished in 0.3 seconds, used 1 MB across 2 packages, added 0.6 KB gzipped, and produced 0 audit findings.
When should you not use class-variance-authority?
The component has a single boolean condition or two short branches. clsx expresses that directly without introducing a variant schema.
Use it if
- A component has several named choices whose allowed values should become TypeScript unions instead of loose strings.
- Compound styling depends on an exact combination, such as a warning tone only when the control is enabled.
- One variant definition must work outside React, including server templates, Vue, Svelte, or static rendering.
- Your design system wants the class table next to the component while leaving CSS generation to Tailwind or ordinary stylesheets.
- The component has a single boolean condition or two short branches. `clsx` expresses that directly without introducing a variant schema.
- Caller-supplied Tailwind utilities must always override defaults. CVA only concatenates classes, so you need a separate cascade convention or `tailwind-merge` for conflict resolution.
- One call must return separate classes for a root, icon, and label. Stable CVA returns one string, while `tailwind-variants` has a slots API for that shape.
- A variant must be required by the library itself. `VariantProps` makes every variant optional; the official TypeScript recipe rebuilds required fields with `Omit`, `Pick`, and `Required`.
- Your release policy requires frequent stable-package updates. npm still marks 0.7.1 from November 2024 as `latest`, while current feature work ships under the separate `cva` beta.
Setup reality
We installed class-variance-authority 0.7.1 in our fresh Node 22 Bookworm sandbox, and npm finished in 0.3 seconds. The result occupied 1 MB across 2 packages. npm audit reported 0 known vulnerabilities: 0 critical, 0 high, 0 moderate, and 0 low. The package itself has 1 direct dependency, no peer dependencies, and 52 KB unpacked under Apache-2.0. Bundled TypeScript declarations were present. Both CommonJS require() and ESM import worked through the exports map. Our esbuild check produced 1.1 KB minified and 0.6 KB gzipped.
Version 0.7.1 needs no account, environment variable, provider, or CVA config file. Variant definitions are plain JavaScript or TypeScript, and Tailwind is optional. Tailwind editor completion inside cva() and cx() does need a classFunctions entry in supported editor settings. That setting changes autocomplete only; it does not change the classes returned at runtime.
CVA passes its inputs to the single clsx dependency and leaves conflicting utilities in the string. If the selector emits px-2 and a caller adds px-6, class order in the HTML is not an override guarantee because Tailwind's generated CSS order decides the result. The official docs give two remedies: a Tailwind 4 base: cascade recipe or a tailwind-merge wrapper. Either choice becomes part of your styling policy.
Defaults apply when a property is omitted or undefined; passing null removes that default for one call. Compound entries require every named condition to match, and an array accepts any listed value. Stable 0.7.1 returns synchronously, stores no cache, and has no built-in slot or composition object. Compose separate selectors with cx, and enforce required variants in your own TypeScript type.
Patterns
Turn button choices into a class selector define-button-variants
import { cva } from 'class-variance-authority';
export const buttonStyles = cva('inline-flex items-center rounded font-medium', {
variants: {
tone: {
primary: 'bg-indigo-600 text-white',
danger: 'bg-red-600 text-white',
},
scale: {
sm: 'h-8 px-3 text-sm',
lg: 'h-11 px-5 text-base',
},
},
});
buttonStyles({ tone: 'primary', scale: 'sm' });`cva()` in 0.7.1 returns a function, and that function produces one class string from the configured variant values.
Fill in choices the caller omits add-default-variants
const tagStyles = cva('inline-flex rounded-full', {
variants: {
tone: { neutral: 'bg-zinc-100', info: 'bg-sky-100' },
scale: { sm: 'px-2 py-0.5 text-xs', md: 'px-3 py-1 text-sm' },
},
defaultVariants: {
tone: 'neutral',
scale: 'md',
},
});
tagStyles();`defaultVariants` is consulted when a value is absent or `undefined`; the returned string includes both defaults in this call.
Map a boolean state to classes model-boolean-variant
const inputStyles = cva('rounded border px-3 py-2', {
variants: {
invalid: {
true: 'border-red-600 text-red-900',
false: 'border-zinc-300',
},
},
defaultVariants: { invalid: false },
});
inputStyles({ invalid: true });The configuration uses `true` and `false` object keys, while `VariantProps` exposes `invalid` as a boolean value.
Add classes for one exact combination add-compound-variant
const alertStyles = cva('rounded border p-4', {
variants: {
tone: { info: 'border-sky-300', danger: 'border-red-300' },
dismissible: { true: 'pr-12', false: null },
},
compoundVariants: [
{
tone: 'danger',
dismissible: true,
class: 'relative font-semibold',
},
],
});A compound rule runs only when every condition in that entry matches; its `class` value is appended after normal variant classes.
Share one compound rule across values target-multiple-values
const chipStyles = cva('rounded-full px-2', {
variants: {
tone: { neutral: 'bg-zinc-100', warning: 'bg-amber-100', danger: 'bg-red-100' },
outlined: { true: 'border', false: null },
},
compoundVariants: [
{
tone: ['warning', 'danger'],
outlined: true,
className: 'border-current',
},
],
});An array inside a compound condition means any listed value can match. Stable 0.7.1 has no inverse or not-equal condition.
Derive props from the variant table extract-variant-props
import { cva, type VariantProps } from 'class-variance-authority';
const textStyles = cva('', {
variants: {
weight: { regular: 'font-normal', bold: 'font-bold' },
muted: { true: 'text-zinc-500', false: 'text-zinc-950' },
},
});
type TextStyleProps = VariantProps<typeof textStyles>;`VariantProps` reads the selector argument type and removes its extra `class` and `className` fields. Configured variants stay optional.
Require one choice with TypeScript require-variant-prop
type InferredProps = VariantProps<typeof buttonStyles>;
type ButtonStyleProps =
Omit<InferredProps, 'tone'> &
Required<Pick<InferredProps, 'tone'>>;
function getButtonClass(props: ButtonStyleProps) {
return buttonStyles(props);
}CVA 0.7.1 has no required-variant flag. The official guide uses TypeScript utility types to make selected fields mandatory.
Connect variant props to a React button use-in-react-component
import type { ButtonHTMLAttributes } from 'react';
import type { VariantProps } from 'class-variance-authority';
type Props = ButtonHTMLAttributes<HTMLButtonElement> &
VariantProps<typeof buttonStyles>;
export function Button({ tone, scale, className, ...rest }: Props) {
return (
<button
{...rest}
className={buttonStyles({ tone, scale, className })}
/>
);
}CVA has no React peer dependency. The component owns DOM props and passes only variant values plus `className` to the selector.
Accept a one-off class from the caller append-caller-classes
const labelStyles = cva('text-sm font-medium', {
variants: {
muted: { true: 'text-zinc-500', false: 'text-zinc-900' },
},
});
labelStyles({
muted: true,
className: 'mb-2 block',
});Every selector accepts `class` or `className` and appends that input. It does not inspect the added utilities for CSS conflicts.
Combine two stable selectors with cx compose-variant-functions
import { cva, cx, type VariantProps } from 'class-variance-authority';
const spacing = cva('', {
variants: { compact: { true: 'p-2', false: 'p-6' } },
});
const surface = cva('rounded', {
variants: { elevated: { true: 'shadow-md', false: 'border' } },
});
type CardStyleProps = VariantProps<typeof spacing> &
VariantProps<typeof surface>;
const cardStyles = ({ compact, elevated }: CardStyleProps = {}) =>
cx(spacing({ compact }), surface({ elevated }));The stable package has no composition method. Its docs combine selector results with `cx`, the package's `clsx` alias.
Resolve Tailwind conflicts after selection merge-tailwind-overrides
import { twMerge } from 'tailwind-merge';
function getButtonClass(
props: Parameters<typeof buttonStyles>[0],
) {
return twMerge(buttonStyles(props));
}
getButtonClass({ scale: 'sm', className: 'px-8' });CVA keeps both padding utilities. `tailwind-merge` is the separate package that makes the later `px-8` replace the earlier horizontal padding.
Remove a default for one call suppress-default-variant
const panelStyles = cva('rounded', {
variants: {
tone: { plain: 'bg-white', muted: 'bg-zinc-100' },
},
defaultVariants: { tone: 'plain' },
});
panelStyles({ tone: null }); // roundedPassing `null` disables the configured default for that invocation. Passing `undefined` would keep the `plain` classes.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| clsx | npm | Use it when you only need conditional class joining and do not need variant prop types or compound rules. |
| tailwind-variants | npm | Use it when a Tailwind component needs typed variants plus named slots for parts such as the icon and label. |
| tailwind-merge | npm | Use it when the job is resolving conflicting Tailwind utilities rather than defining a component variant table. |
More web frontend guides
postcss · react · react-dom · tailwindcss · htmlparser2 · tailwind-merge · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

