Skip to content

css-variants is no longer maintained. Use @lynstack/class-recipe: faster, stricter types, and React Native support. Migrate →

No longer maintained. Migrate to class-recipe →

Migrate to class-recipe

  • Faster. A recipe compiles its config once and caches the result of each selection, so most calls are a lookup. See its benchmarks.
  • Stricter types. Variant names, options, and slots are inferred from the config. An unknown option or slot is a type error, and a variant without a default is required, so a component cannot forget to choose it.
  • Better structure. Class names, style objects, and React Native styles share one engine, @lynstack/recipe, with the same config everywhere.
  • React Native. @lynstack/native-recipe builds React Native style objects, and returns the same frozen style for the same variants, so the style prop keeps its identity between renders.
  • Maintained, with docs, migration guides, and an agent skill for coding agents.
Terminal window
npm uninstall css-variants
npm install @lynstack/class-recipe

class-recipe ships as an ES module only and requires TypeScript 5.4 or newer for its types.

css-variantsReplacement
cx()cx from @lynstack/class-recipe
cv()cva from @lynstack/class-recipe
scv()sva from @lynstack/class-recipe
classNameResolver optioncreateRecipes({ join }), such as createRecipes({ join: twMerge })
sv(), ssv() in React NativecreateStyleRecipe and createSlotStyleRecipe from @lynstack/native-recipe
sv(), ssv() on the webA recipe kind of @lynstack/recipe that merges style objects

The config keeps base, variants, compoundVariants, and defaultVariants. A compound variant moves its conditions under variants:

import { cv } from 'css-variants'
const button = cv({
base: 'rounded font-medium',
variants: {
color: {
primary: 'bg-blue-600 text-white',
secondary: 'bg-gray-200 text-gray-800',
},
size: {
sm: 'px-3 py-1.5 text-sm',
lg: 'px-6 py-3 text-lg',
},
},
compoundVariants: [{ color: 'primary', size: 'lg', className: 'shadow-lg' }],
defaultVariants: { color: 'primary', size: 'sm' },
})

Slot recipes move the same way: scv becomes sva, with the same slots, and a compound variant keeps its classNames and moves its conditions under variants.

  • A variant without a default is required. Give it a default, or pass it on every call.
  • Classes are strings. Wherever the config takes classes, pass one string; cx(...) turns an array or an object into one.

The class-recipe docs describe every option, and its guides to migrating from class-variance-authority and tailwind-variants cover the same concepts in more detail.