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 →
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 →
Use a CSS variant library like css-variants when you need to:
css-variants is ideal for design systems where you need consistent, reusable components:
import { cv } from 'css-variants'
// Define your button variants onceexport const button = cv({ base: 'inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2', variants: { variant: { primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500', secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:ring-gray-500', outline: 'border-2 border-gray-300 bg-transparent hover:bg-gray-100 focus:ring-gray-500', ghost: 'bg-transparent hover:bg-gray-100 focus:ring-gray-500', destructive: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500', }, size: { sm: 'h-8 px-3 text-sm rounded-md', md: 'h-10 px-4 text-sm rounded-md', lg: 'h-12 px-6 text-base rounded-lg', icon: 'h-10 w-10 rounded-md', }, disabled: { true: 'opacity-50 cursor-not-allowed pointer-events-none', }, }, defaultVariants: { variant: 'primary', size: 'md', },})
// Reuse across your application<button className={button({ variant: 'primary', size: 'lg' })}> Get Started</button>
<button className={button({ variant: 'ghost', size: 'icon' })}> <Icon /></button>Use scv when a component has multiple styled parts:
import { scv } from 'css-variants'
const modal = scv({ slots: ['overlay', 'container', 'header', 'body', 'footer', 'closeButton'], base: { overlay: 'fixed inset-0 bg-black/50 backdrop-blur-sm', container: 'fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-lg shadow-xl', header: 'flex items-center justify-between px-6 py-4 border-b', body: 'px-6 py-4', footer: 'flex justify-end gap-2 px-6 py-4 border-t', closeButton: 'absolute top-4 right-4 text-gray-400 hover:text-gray-600', }, variants: { size: { sm: { container: 'w-80' }, md: { container: 'w-[480px]' }, lg: { container: 'w-[640px]' }, full: { container: 'w-[90vw] max-w-4xl' }, }, }, defaultVariants: { size: 'md', },})
// In your componentconst classes = modal({ size: 'lg' })
<div className={classes.overlay}> <div className={classes.container}> <header className={classes.header}>...</header> <div className={classes.body}>...</div> <footer className={classes.footer}>...</footer> <button className={classes.closeButton}>×</button> </div></div>Use sv when you need CSS style objects instead of class names:
import { sv } from 'css-variants'
// Perfect for dynamic values that can't be utility classesconst progressBar = sv({ base: { height: '8px', borderRadius: '4px', backgroundColor: '#e5e7eb', overflow: 'hidden', }, variants: { color: { blue: { '--progress-color': '#3b82f6' }, green: { '--progress-color': '#22c55e' }, red: { '--progress-color': '#ef4444' }, }, },})
// CSS custom properties work great with sv<div style={{ ...progressBar({ color: 'blue' }), '--progress-width': `${progress}%`, }}> <div style={{ width: 'var(--progress-width)', backgroundColor: 'var(--progress-color)' }} /></div>Replace complex conditional logic with clean variant calls:
// Before: Messy conditional logicfunction Button({ isPrimary, isLarge, isDisabled, isLoading }) { const className = [ 'btn', isPrimary ? 'btn-primary' : 'btn-secondary', isLarge ? 'btn-lg' : 'btn-md', isDisabled && 'btn-disabled', isLoading && 'btn-loading', ].filter(Boolean).join(' ')
return <button className={className}>...</button>}
// After: Clean variant-based stylingimport { cv } from 'css-variants'
const button = cv({ base: 'btn', variants: { variant: { primary: 'btn-primary', secondary: 'btn-secondary' }, size: { lg: 'btn-lg', md: 'btn-md' }, disabled: { true: 'btn-disabled' }, loading: { true: 'btn-loading' }, }, defaultVariants: { variant: 'secondary', size: 'md' },})
function Button({ variant, size, disabled, loading }) { return <button className={button({ variant, size, disabled, loading })}>...</button>}| Requirement | Why css-variants |
|---|---|
| Best performance | 3-11x faster than CVA and tailwind-variants |
| Smallest bundle | ~1KB vs 2-5KB for alternatives |
| Zero dependencies | No clsx or tailwind-merge bundled |
| Multi-slot components | Built-in scv function |
| Inline style variants | Unique sv and ssv functions |
| Framework flexibility | Works with React, Vue, Svelte, Solid, vanilla JS |
| CSS flexibility | Works with Tailwind, CSS Modules, vanilla CSS |
extend property for component compositionNot necessarily. css-variants is most valuable for:
scv)For simple components with no variants, regular class strings are fine:
// This is fine — no need for css-variantsfunction Divider() { return <hr className="my-4 border-gray-200" />}Yes! css-variants is additive. You can:
Yes. css-variants generates class strings at runtime with no DOM dependencies. It works perfectly with:
Yes. css-variants is:
Use css-variants when you want:
scv for complex componentssv and ssv functionsReady to start? See the Quick Start Guide.