A lightweight utility for generating class names based on variant configurations. Inspired by Tailwind and class-variance-authority
, @gentelduck/variants
helps you manage and compose class names in a flexible, type-safe, and declarative way.
At GentleDuck, we believe that developer tools should be fast, reliable, and actively maintained — not abandoned. While class-variance-authority
initially inspired the idea of variant-based class management, its situation has become unacceptable:
despite having over 6 million weekly downloads, the project has been left poorly maintained for more than 6 months, with important pull requests and bug fixes sitting untouched.
Leaving a critical utility like this in a broken or half-maintained state is unacceptable to us at GentleDuck.
So, we took action: we rewrote the library from scratch with our own vision and philosophy — making it more modern, more reliable, and up to 7× faster.
Our goal with @gentelduck/variants is simple:
to offer the community a serious, well-maintained, and future-proof alternative — one that developers can trust today and for the years to come.
- 🧠 Declarative variant-based styling
- 🎯 Type-safe API with intelligent autocompletion
- 🧱 Composable and extendable utility
- 🎨 Supports default variants and compound variants
- 🪶 Lightweight, zero dependencies, blazing fast
- ✅ Zero dependencies, tiny bundle footprint.
- 🔐 Fully type-safe, IDE-friendly autocompletion.
- 🎨 Flexible styling via variants, nested arrays & conditionals.
- 🚀 Minimal runtime & memoized with zero runtime dependencies (just a few dozen lines of code).
- ⚡ Blazing fast: ~7× faster than the reference
(class-variance-authority)[https://www.npmjs.com/package/class-variance-authority]
. - 🧠 Powerful system for defaults, compounds, and custom classes.
@gentelduck/variants: ~7x faster than (class-variance-authority)[https://www.npmjs.com/package/class-variance-authority]
npm install @gentelduck/variants
# or
yarn add @gentelduck/variants
# or
pnpm add @gentelduck/variants
@gentelduck/variants
lets you declare your style variants once and get back a function that produces perfectly composed class names, complete with defaults, compounds, nested arrays, and conditional objects.
import { cva } from '@gentelduck/variants'
const button = cva('btn', {
variants: {
size: {
sm: 'px-2 py-1 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
},
color: {
primary: 'bg-blue-500 text-white',
secondary: 'bg-gray-100 text-gray-800',
},
},
defaultVariants: {
size: 'md',
color: 'primary',
},
compoundVariants: [
{ size: 'lg', color: 'primary', className: 'uppercase font-bold' },
],
})
button()
// => 'btn px-4 py-2 text-base bg-blue-500 text-white uppercase font-bold'
button({ size: 'sm', class: ['rounded', 'shadow'] })
// => 'btn px-2 py-1 text-sm bg-blue-500 text-white rounded shadow'
You can supply arrays of classes or nested arrays for fine-grained control:
const badge = cva('badge', {
variants: {
size: {
sm: ['badge-sm', 'text-xs'],
lg: ['badge-lg', 'text-lg'],
},
tone: {
muted: ['opacity-50', ['italic']],
loud: ['opacity-100', { 'font-bold': true }],
},
},
defaultVariants: {
size: 'sm',
tone: 'muted',
},
})
badge({ size: 'lg', tone: 'loud' })
// => 'badge badge-lg text-lg opacity-100 font-bold'
Apply extra classes when multiple variant values match:
const card = cva('card', {
variants: {
size: { small: 'card-sm', large: 'card-lg' },
color: { primary: 'card-primary', secondary: 'card-secondary' },
},
defaultVariants: { size: 'small', color: 'primary' },
compoundVariants: [
{ size: 'large', color: 'primary', class: 'shadow-xl' },
{ size: 'small', color: ['secondary', 'primary'], className: 'border-dashed' },
],
})
card({ size: 'large', color: 'primary' })
// => 'card card-lg card-primary shadow-xl'
Built from the ground up with TypeScript:
- Autocompletion for variant keys & values
- Strict checks: invalid variant names/values will error at compile time
-
Utility types like
VariantProps<>
to extract only variant-related props
const alert = cva('alert', {
variants: {
severity: {
info: 'alert-info',
error: 'alert-error',
},
},
defaultVariants: {
severity: 'info',
},
})
// ✅
alert({ severity: 'error' })
// ❌ TS Error: 'warning' is not a valid severity
alert({ severity: 'warning' })
Compose one CVA function with another, or extend defaults:
const baseButton = cva('btn', {
variants: { size: { sm: 'btn-sm', lg: 'btn-lg' } },
defaultVariants: { size: 'sm' },
})
const largePrimary = cva(baseButton, {
defaultVariants: { size: 'lg' },
})
largePrimary({ className: 'mx-2' })
// => 'btn btn-lg mx-2'
You can also simply concatenate:
const icon = cva('icon', { variants: { type: { arrow: 'icon-arrow' } } })
`${button()} ${icon({ type: 'arrow' })}`
// => 'btn px-4 py-2 text-base bg-blue-500 text-white uppercase font-bold icon icon-arrow'
Creates your CVA function:
type CvaFn<T> = (
props?: VariantProps<T> & {
class?: ClassValue
className?: ClassValue
}
) => string
-
base: string
— always-included classes -
variants
— map of variant names → (value → class/string[]) -
defaultVariants?
— fallback values -
compoundVariants?
— apply extra classes when multiple values match
Overloads:
// Two-arg form
const fn = cva('base', { variants, defaultVariants, compoundVariants })
// Single-arg form
const fn = cva({ base: 'base', variants, defaultVariants, compoundVariants })
MIT © GentleDuck