Skip to content

FAQ

A CSS variant library is a JavaScript/TypeScript tool that helps you manage component styling through variants. Instead of writing conditional class logic like className={isPrimary ? 'btn-primary' : 'btn-secondary'}, you define variants declaratively and the library generates the correct classes.

css-variants provides functions like cv() to define variants:

const button = cv({
base: 'btn',
variants: {
color: {
primary: 'btn-primary',
secondary: 'btn-secondary',
},
},
})
// Clean API
button({ color: 'primary' }) // => 'btn btn-primary'

css-variants is a zero-dependency, type-safe library for managing CSS class variants in JavaScript/TypeScript applications. It provides:

  • cv() — Create variants for single-element components (returns class string)
  • scv() — Create variants for multi-slot components (returns object of class strings)
  • sv() — Create variants for inline styles (returns style object)
  • ssv() — Create variants for multi-slot inline styles (returns object of style objects)
  • cx() — Merge class names conditionally (like clsx)

css-variants is ideal for:

  • React/Vue/Svelte developers building component libraries
  • Teams using Tailwind CSS who want organized variant management
  • Anyone using utility-first CSS (not limited to Tailwind)
  • Developers who value performance — css-variants is 3-11x faster than alternatives
  • TypeScript users who want full type inference for variant props

css-variants has a very similar API to CVA (Class Variance Authority), with key differences:

FeatureCVAcss-variants
Base stylesFirst argumentbase property
Compound class keyclassclassName
Bundle size~2KB~1KB
Slot variantsNot built-inscv() function
Style variantsNot built-insv() and ssv() functions
PerformanceBaseline3-7x faster (compound variants)

See the Migration Guide for detailed migration steps.

css-variants offers several advantages over CVA:

  1. Performance: 3-7x faster for compound variants and complex components
  2. Bundle size: ~1KB vs ~2KB (50% smaller)
  3. More features: Built-in slot variants and style variants
  4. Zero dependencies: No clsx bundled (you can add it if needed)

If you’re starting a new project or care about performance, css-variants is the better choice.

Is css-variants better than tailwind-variants?

Section titled “Is css-variants better than tailwind-variants?”

css-variants offers significant improvements over tailwind-variants:

  1. Performance: 5-11x faster across all benchmarks
  2. Bundle size: ~1KB vs ~5KB (80% smaller)
  3. Zero dependencies: No bundled tailwind-merge
  4. Unique features: Style variants (sv, ssv) for inline CSS

Choose tailwind-variants if you need:

  • Built-in tailwind-merge (css-variants requires opt-in)
  • Component composition via extend property

The best CSS variant library depends on your priorities:

PriorityBest ChoiceWhy
Performancecss-variants3-11x faster than alternatives
Bundle sizecss-variants~1KB, smallest available
Ecosystem/tutorialsCVAMore established, more examples
Built-in Tailwind mergetailwind-variantsAutomatic class conflict resolution

For most projects, css-variants is the best choice due to its combination of performance, small size, and comprehensive features.

Yes, css-variants is a drop-in replacement for CVA. The migration is straightforward:

import { cva } from 'class-variance-authority'
import { cv } from 'css-variants'
const button = cva('base-classes', {
const button = cv({
base: 'base-classes',
variants: { /* unchanged */ },
compoundVariants: [
{ color: 'primary', class: 'extra' }
{ color: 'primary', className: 'extra' }
],
})

Can I use css-variants without Tailwind CSS?

Section titled “Can I use css-variants without Tailwind CSS?”

Absolutely yes! css-variants is framework-agnostic and works with any CSS approach:

  • Vanilla CSS: Use regular class names
  • CSS Modules: Import and use module class names
  • CSS-in-JS: Use with styled-components, emotion, etc.
  • Any utility framework: Bootstrap, Bulma, UnoCSS, etc.
// Vanilla CSS
const button = cv({
base: 'btn',
variants: {
color: {
primary: 'btn-primary',
secondary: 'btn-secondary',
},
},
})
// CSS Modules
import styles from './Button.module.css'
const button = cv({
base: styles.button,
variants: {
color: {
primary: styles.primary,
secondary: styles.secondary,
},
},
})

Yes! css-variants works excellently with React:

import { cv } from 'css-variants'
const button = cv({
base: 'px-4 py-2 rounded font-medium',
variants: {
variant: {
primary: 'bg-blue-600 text-white',
secondary: 'bg-gray-200 text-gray-800',
},
},
})
// Extract variant types for props
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
Parameters<typeof button>[0]
function Button({ variant, className, ...props }: ButtonProps) {
return <button className={button({ variant, className })} {...props} />
}

Yes! css-variants integrates smoothly with Vue 3:

<script setup lang="ts">
import { cv } from 'css-variants'
const button = cv({
base: 'px-4 py-2 rounded font-medium',
variants: {
variant: {
primary: 'bg-blue-600 text-white',
secondary: 'bg-gray-200 text-gray-800',
},
},
})
defineProps<{ variant?: 'primary' | 'secondary' }>()
</script>
<template>
<button :class="button({ variant })">
<slot />
</button>
</template>

Yes! css-variants works with Svelte:

<script lang="ts">
import { cv } from 'css-variants'
const button = cv({
base: 'px-4 py-2 rounded font-medium',
variants: {
variant: {
primary: 'bg-blue-600 text-white',
secondary: 'bg-gray-200 text-gray-800',
},
},
})
export let variant: 'primary' | 'secondary' = 'primary'
</script>
<button class={button({ variant })}>
<slot />
</button>

What is the difference between cv, scv, sv, and ssv?

Section titled “What is the difference between cv, scv, sv, and ssv?”
FunctionOutput TypeUse Case
cv()stringSingle-element components (buttons, badges)
scv(){ [slot]: string }Multi-slot components (cards, modals, dropdowns)
sv()CSSPropertiesSingle-element inline styles
ssv(){ [slot]: CSSProperties }Multi-slot inline styles
// cv - returns string
button({ color: 'primary' }) // => 'btn btn-primary'
// scv - returns object of strings
card({ variant: 'default' }) // => { root: '...', header: '...', body: '...' }
// sv - returns style object
box({ size: 'lg' }) // => { padding: '24px', borderRadius: '8px' }
// ssv - returns object of style objects
tooltip({ placement: 'top' }) // => { root: {...}, arrow: {...} }

Use sv() (style variants) when you need inline CSS styles instead of class names:

  • Dynamic CSS values: width: ${value}px that can’t be utility classes
  • CSS custom properties: CSS variables like --color: ${color}
  • Third-party integrations: Libraries expecting style objects
  • Canvas/SVG styling: Elements that use style attribute
import { sv } from 'css-variants'
const dynamicBox = sv({
base: { display: 'flex' },
variants: {
size: {
sm: { width: '100px', height: '100px' },
md: { width: '200px', height: '200px' },
},
},
})
// Use with style prop
<div style={dynamicBox({ size: 'md' })} />

How do I create a variant for a multi-element component?

Section titled “How do I create a variant for a multi-element component?”

Use scv() (slot class variants) for components with multiple styled elements:

import { scv } from 'css-variants'
const card = scv({
slots: ['root', 'header', 'content', 'footer'],
base: {
root: 'rounded-lg border',
header: 'p-4 border-b',
content: 'p-4',
footer: 'p-4 border-t',
},
variants: {
variant: {
default: { root: 'border-gray-200' },
primary: { root: 'border-blue-200' },
},
},
})
// Usage
const classes = card({ variant: 'primary' })
// In JSX
<div className={classes.root}>
<header className={classes.header}>Title</header>
<div className={classes.content}>Content</div>
<footer className={classes.footer}>Footer</footer>
</div>

Use tailwind-merge with a custom class resolver:

import { cv, cx } from 'css-variants'
import { twMerge } from 'tailwind-merge'
const button = cv({
base: 'px-4 py-2',
variants: {
size: {
lg: 'px-6 py-3', // Would conflict with base without twMerge
},
},
classNameResolver: (...args) => twMerge(cx(...args)),
})

See the Tailwind Integration Guide for more details.

Use Tailwind’s responsive prefixes in your variant classes:

const container = cv({
variants: {
size: {
sm: 'max-w-screen-sm px-4',
md: 'max-w-screen-md px-6 sm:px-8',
lg: 'max-w-screen-lg px-8 sm:px-10 lg:px-12',
},
},
})

For different variants at different breakpoints, combine with cx:

<div className={cx(
container({ size: 'sm' }),
'md:max-w-screen-md lg:max-w-screen-lg'
)}>

Two approaches:

1. Use Tailwind’s dark: prefix:

const card = cv({
base: 'bg-white dark:bg-gray-800 text-gray-900 dark:text-white',
})

2. Create explicit theme variants:

const card = cv({
variants: {
theme: {
light: 'bg-white text-gray-900',
dark: 'bg-gray-800 text-white',
},
},
})

Does css-variants work with Tailwind’s @apply?

Section titled “Does css-variants work with Tailwind’s @apply?”

Yes, but we recommend using variants instead of @apply for better tree-shaking and smaller CSS bundles:

/* Works but not recommended */
.btn-primary {
@apply bg-blue-600 text-white hover:bg-blue-700;
}
/* Recommended */
const button = cv({
variants: {
color: {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
},
},
})

Use TypeScript’s Parameters utility type:

const button = cv({
variants: {
color: { primary: '...', secondary: '...' },
size: { sm: '...', md: '...', lg: '...' },
},
})
type ButtonVariants = Parameters<typeof button>[0]
// => { color?: 'primary' | 'secondary', size?: 'sm' | 'md' | 'lg', className?: ClassValue }

Use TypeScript utility types:

type ButtonVariants = Parameters<typeof button>[0]
type RequiredColor = Omit<ButtonVariants, 'color'> &
Required<Pick<ButtonVariants, 'color'>>

JavaScript object keys must be strings or symbols. We use 'true' and 'false' as keys, but TypeScript infers the prop types as actual booleans:

const toggle = cv({
variants: {
enabled: {
true: 'bg-blue-600', // String key
false: 'bg-gray-200', // String key
},
},
})
// Props accept actual booleans
toggle({ enabled: true }) // Works
toggle({ enabled: 'true' }) // TypeScript error

Yes! Use cx to compose:

import { cv, cx } from 'css-variants'
const baseButton = cv({
base: 'rounded font-medium',
variants: {
size: {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
},
},
})
const primaryButton = (props) => cx(
baseButton(props),
'bg-blue-600 text-white hover:bg-blue-700'
)

Or extend by using one variant as the base of another:

const iconButton = cv({
base: baseButton({ size: 'md' }),
variants: {
rounded: {
default: 'rounded-lg',
full: 'rounded-full',
},
},
})

Define reusable configuration objects:

const sharedVariants = {
size: {
sm: 'text-sm px-2 py-1',
md: 'text-base px-4 py-2',
lg: 'text-lg px-6 py-3',
},
} as const
const button = cv({
base: 'rounded font-medium',
variants: {
...sharedVariants,
color: { primary: '...', secondary: '...' },
},
})
const badge = cv({
base: 'rounded-full',
variants: {
...sharedVariants,
color: { info: '...', warning: '...' },
},
})

This happens when variants have conflicting classes. Use tailwind-merge:

import { twMerge } from 'tailwind-merge'
const button = cv({
base: 'px-4',
variants: {
size: { lg: 'px-6' }, // Without twMerge: 'px-4 px-6'
},
classNameResolver: (...args) => twMerge(cx(...args)),
})

TypeScript isn’t inferring my variant types

Section titled “TypeScript isn’t inferring my variant types”

Make sure you’re:

  1. Using TypeScript 4.7 or later
  2. Not using as const on the config object (it breaks inference)
  3. Defining variants directly in the config, not as a separate variable
// Good
const button = cv({
variants: {
color: { primary: '...', secondary: '...' },
},
})
// Bad: Separate variable loses type inference
const variants = {
color: { primary: '...', secondary: '...' },
}
const button = cv({ variants })

My slot variant isn’t applying classes to all slots

Section titled “My slot variant isn’t applying classes to all slots”

Make sure you’re:

  1. Defining all slots in the slots array
  2. Using the correct slot names in base and variants
  3. Destructuring all needed slots from the result
const card = scv({
slots: ['root', 'header', 'content'], // Must list all slots
base: {
root: '...',
header: '...',
content: '...',
},
})
const { root, header, content } = card({ variant: 'default' })

css-variants is the fastest CSS variant library available:

vs CVAvs tailwind-variants
3-4x faster (compound variants)5-7x faster (compound variants)
2-7x faster (complex components)10-11x faster (complex components)

See detailed benchmarks.

css-variants achieves superior performance through:

  1. Optimized data structures: Simple objects/arrays, no class instances
  2. Early exit strategies: Stops compound variant checks at first mismatch
  3. Pre-computation: Analyzes configs at creation time, not runtime
  4. Minimal string operations: Efficient class concatenation
  5. Zero dependencies: No function call overhead from external libraries

css-variants is ~1KB minified + gzipped — the smallest variant library available:

LibraryBundle Size
css-variants~1KB
CVA~2KB
tailwind-variants~5KB

You can also use selective imports for even smaller bundles:

import { cv } from 'css-variants/cv' // Only cv function
import { scv } from 'css-variants/scv' // Only scv function