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 →
css-variants works seamlessly with Tailwind CSS. Use Tailwind utility classes directly in your variant definitions.
Use Tailwind classes directly in your variants:
import { cv } from 'css-variants'
const button = cv({ base: 'font-semibold rounded-lg transition-colors focus:outline-none focus:ring-2', variants: { color: { primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500', secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500', danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500', }, size: { sm: 'px-3 py-1.5 text-sm', md: 'px-4 py-2 text-base', lg: 'px-6 py-3 text-lg', }, }, defaultVariants: { color: 'primary', size: 'md', },})By default, css-variants concatenates all classes. This can cause issues when Tailwind classes conflict:
const button = cv({ base: 'px-4 py-2 text-sm', variants: { size: { lg: 'px-6 py-3 text-lg', // Conflicts with base padding/text }, },})
button({ size: 'lg' })// => 'px-4 py-2 text-sm px-6 py-3 text-lg'// Both px-4 and px-6 are present - last one wins in CSS, but it's messyInstall tailwind-merge to resolve class conflicts:
npm install tailwind-mergeCreate a custom class resolver:
import { cv, cx } from 'css-variants'import { twMerge } from 'tailwind-merge'
const classNameResolver: typeof cx = (...args) => twMerge(cx(...args))
const button = cv({ base: 'px-4 py-2 text-sm', variants: { size: { lg: 'px-6 py-3 text-lg', }, }, classNameResolver,})
button({ size: 'lg' })// => 'px-6 py-3 text-lg' (conflicts resolved!)Create a utility file to centralize your variant creators:
import css from 'css-variants'import { twMerge } from 'tailwind-merge'
// Enhanced cx that merges Tailwind classesexport const cx: typeof css.cx = (...args) => twMerge(css.cx(...args))
// Enhanced cv with automatic Tailwind mergingexport const cv: typeof css.cv = (config) => css.cv({ ...config, classNameResolver: cx,})
// Enhanced scv with automatic Tailwind mergingexport const scv: typeof css.scv = (config) => css.scv({ ...config, classNameResolver: cx,})Use throughout your project:
import { cv } from '@/lib/variants'
const button = cv({ base: 'px-4 py-2 rounded', variants: { // Your variants... },})Use Tailwind’s responsive prefixes in your variant classes:
const container = cv({ base: 'w-full mx-auto', variants: { size: { sm: 'max-w-screen-sm px-4', md: 'max-w-screen-md px-6', lg: 'max-w-screen-lg px-8', xl: 'max-w-screen-xl px-8', }, }, defaultVariants: { size: 'lg', },})For different variants at different breakpoints, combine with cx:
import { cx } from '@/lib/variants'
<div className={cx( container({ size: 'sm' }), 'md:max-w-screen-md lg:max-w-screen-lg')}> Content</div>const card = cv({ base: 'rounded-lg p-6 bg-white dark:bg-gray-800 text-gray-900 dark:text-white', variants: { variant: { default: 'border border-gray-200 dark:border-gray-700', elevated: 'shadow-lg dark:shadow-gray-900/50', }, },})Alternatively, create explicit theme variants for more control:
const card = cv({ base: 'rounded-lg p-6', variants: { theme: { light: 'bg-white text-gray-900 border-gray-200', dark: 'bg-gray-800 text-white border-gray-700', }, variant: { default: 'border', elevated: 'shadow-lg', }, }, compoundVariants: [ { theme: 'dark', variant: 'elevated', className: 'shadow-gray-900/50', }, ],})Use Tailwind’s arbitrary value syntax w-[42px] in variants:
const avatar = cv({ base: 'rounded-full object-cover', variants: { size: { xs: 'w-6 h-6', sm: 'w-8 h-8', md: 'w-10 h-10', lg: 'w-12 h-12', custom: 'w-[42px] h-[42px]', // Arbitrary value }, },})const card = cv({ base: 'group rounded-lg border p-4 transition-all', variants: { interactive: { true: 'cursor-pointer hover:shadow-lg hover:border-blue-500', false: '', }, },})
const cardTitle = cv({ base: 'text-lg font-semibold transition-colors', variants: { interactive: { true: 'group-hover:text-blue-600', false: '', }, },})import { scv } from '@/lib/variants'
const dropdown = scv({ slots: ['trigger', 'menu', 'item'], base: { trigger: 'inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-white border', menu: 'absolute mt-2 w-48 rounded-lg bg-white shadow-lg border py-1', item: 'block px-4 py-2 text-sm hover:bg-gray-100 transition-colors', }, variants: { size: { sm: { trigger: 'px-3 py-1.5 text-sm', item: 'px-3 py-1.5 text-xs', }, lg: { trigger: 'px-6 py-3 text-lg', menu: 'w-64', item: 'px-6 py-3 text-base', }, }, },})For Tailwind CSS autocomplete in VS Code, install the Tailwind CSS IntelliSense extension.
Add to your .vscode/settings.json:
{ "tailwindCSS.classFunctions": ["cv", "scv", "cx"]}This enables autocomplete for Tailwind classes inside cv(), scv(), and cx() function calls.
If you use prettier-plugin-tailwindcss to sort Tailwind classes, add cv, scv, and cx to the plugin configuration:
module.exports = { plugins: ['prettier-plugin-tailwindcss'], tailwindFunctions: ['cv', 'scv', 'cx'],}This ensures your Tailwind classes are automatically sorted within css-variants function calls.