react-class-variants
v2.0.0-alpha.5
Published
Type-safe React variants API for dynamic CSS class composition
Maintainers
Readme
React Class Variants
A lightweight, type-safe library for building composable React components with dynamic CSS class variations. Works seamlessly with Tailwind CSS, CSS Modules, or any CSS solution.
Important
react-tailwind-variantswas renamed toreact-class-variants. The v2 line is currently published on thealphachannel, so the recommended install command isreact-class-variants@alpha. The legacyreact-tailwind-variantspackage is frozen and kept only for migration and maintenance notices. Start with the Migration Guide and keep the Legacy v1 Docs handy while migrating.
Why React Class Variants?
Building UI components often requires managing multiple visual states and combinations. React Class Variants provides a powerful API inspired by Stitches.js that makes this trivial:
// Define variants once
const Button = variantComponent('button', {
variants: {
color: {
primary: 'bg-blue-500 text-white',
secondary: 'bg-gray-500 text-white',
},
size: {
sm: 'px-3 py-1 text-sm',
lg: 'px-6 py-3 text-lg',
},
},
});
// Use anywhere with full type safety
<Button color="primary" size="lg">
Click me
</Button>;No more messy className logic, no more props duplication, just clean, type-safe components.
Features
- 🎯 Type-Safe - Automatic TypeScript inference for all variant combinations
- 🎨 Flexible - Works with Tailwind CSS, CSS Modules, or plain CSS classes
- ⚡ Lightweight - Zero dependencies (~2KB minified + gzipped)
- 🔀 Compound Variants - Apply styles based on multiple variant combinations
- 🎭 Polymorphic - Render components as different elements with full type safety
- 🔧 Smart Merging - Optional class conflict resolution via
tailwind-mergeor custom function - 📦 Tree-Shakeable - Import only what you need
- ⚛️ React 19 Ready - Full support for modern React
Table of Contents
- Why React Class Variants?
- Features
- Migration
- Installation
- Quick Start
- Tailwind CSS IntelliSense
- Core Concepts
- API Reference
- TypeScript
- Usage with Different CSS Solutions
- Real-World Examples
- Advanced Patterns
- Performance
- Comparison
- Release Process
- Contributing
- License
- Links
Migration
- Renamed package:
react-tailwind-variants->react-class-variants - Current release channel:
alpha - Recommended install for v2:
npm install react-class-variants@alpha - Migration guide: docs/migration-from-react-tailwind-variants.md
- Legacy v1 docs: docs/react-tailwind-variants-v1.md
Installation
Compatibility:
- React
19+ - Node.js
20.19+ - Current v2 install channel:
react-class-variants@alpha
npm install react-class-variants@alphayarn add react-class-variants@alphapnpm add react-class-variants@alphaOptional: For Tailwind CSS class conflict resolution:
npm install tailwind-mergeQuick Start
1. Basic Usage
import { defineConfig } from 'react-class-variants';
const { variants } = defineConfig();
// Create a variant function
const buttonClasses = variants({
base: 'font-semibold rounded transition',
variants: {
color: {
blue: 'bg-blue-500 text-white hover:bg-blue-600',
gray: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
},
},
});
// Use it
function MyButton() {
return <button className={buttonClasses({ color: 'blue' })}>Click me</button>;
}2. Creating Components
import { defineConfig } from 'react-class-variants';
const { variantComponent } = defineConfig();
const Button = variantComponent('button', {
base: 'font-semibold rounded transition',
variants: {
color: {
blue: 'bg-blue-500 text-white hover:bg-blue-600',
gray: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
},
size: {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2',
lg: 'px-6 py-3 text-lg',
},
},
defaultVariants: {
color: 'blue',
size: 'md',
},
});
// Component is fully typed!
function App() {
return (
<Button color="gray" size="lg">
Hello
</Button>
);
}3. With Tailwind Merge (Recommended for Tailwind CSS)
import { defineConfig } from 'react-class-variants';
import { twMerge } from 'tailwind-merge';
// Configure once for your entire app
const { variants, variantComponent } = defineConfig({
onClassesMerged: twMerge, // Handles conflicting Tailwind classes
});
const Button = variantComponent('button', {
base: 'px-4 py-2', // These get properly merged...
variants: {
spacing: {
tight: 'px-2 py-1', // ...with these
wide: 'px-8 py-4',
},
},
});
// px-4 from base is overridden by px-2 from variant
<Button spacing="tight" />;Tailwind CSS IntelliSense
If you're using Tailwind CSS, you can enable autocompletion and syntax highlighting for class names inside your variant configurations.
Setting Up VS Code
Install the Tailwind CSS IntelliSense extension for VS Code
Add the following configuration to your VS Code
settings.json:
{
"tailwindCSS.classFunctions": [
"variants",
"variantPropsResolver",
"variantComponent"
]
}- Now you'll get full IntelliSense support in your variant configurations:
import { defineConfig } from 'react-class-variants';
const { variantComponent } = defineConfig();
const Button = variantComponent('button', {
base: 'px-5 py-2 text-white transition-colors',
variants: {
color: {
neutral: 'bg-slate-500 hover:bg-slate-400', // Full IntelliSense here
accent: 'bg-teal-500 hover:bg-teal-400',
},
size: {
sm: 'text-sm',
lg: 'text-lg',
},
},
});You'll get:
- Autocompletion for Tailwind classes
- Hover previews showing the actual CSS
- Linting for invalid or conflicting classes
- Color decorators
Core Concepts
Variants
Variants are different visual states of a component:
const alert = variants({
variants: {
variant: {
info: 'bg-blue-100 text-blue-900 border-blue-200',
success: 'bg-green-100 text-green-900 border-green-200',
warning: 'bg-yellow-100 text-yellow-900 border-yellow-200',
error: 'bg-red-100 text-red-900 border-red-200',
},
},
});
alert({ variant: 'success' }); // Returns success classesBoolean Variants
Use "true" and "false" string keys for boolean props:
const button = variants({
variants: {
outlined: {
true: 'border-2 bg-transparent',
false: 'border-0',
},
disabled: {
true: 'opacity-50 cursor-not-allowed',
},
},
});
// Usage
<Button outlined /> // outlined: true
<Button outlined={false} /> // outlined: false
<Button disabled /> // disabled: trueCompound Variants
Apply styles when multiple variants match:
const button = variants({
variants: {
color: {
primary: 'bg-blue-500',
secondary: 'bg-gray-500',
},
size: {
sm: 'text-sm',
lg: 'text-lg',
},
},
compoundVariants: [
{
variants: {
color: 'primary',
size: 'lg',
},
className: 'font-bold shadow-lg',
},
],
});
// Gets: bg-blue-500 + text-lg + font-bold shadow-lg
button({ color: 'primary', size: 'lg' });Compound variants support array matching (OR condition):
compoundVariants: [
{
variants: {
color: ['primary', 'secondary'], // Matches if primary OR secondary
size: 'lg',
},
className: 'uppercase',
},
];Default Variants
Make variants optional by providing defaults:
const button = variants({
variants: {
color: {
primary: 'bg-blue-500',
secondary: 'bg-gray-500',
},
size: {
sm: 'text-sm',
md: 'text-base',
lg: 'text-lg',
},
},
defaultVariants: {
color: 'primary', // Now optional
size: 'md', // Now optional
},
});
// All equivalent:
button({});
button({ color: 'primary' });
button({ size: 'md' });
button({ color: 'primary', size: 'md' });Polymorphic Components
Render components as different elements while preserving styles:
const Button = variantComponent('button', {
base: 'px-4 py-2 rounded font-semibold',
variants: {
color: {
primary: 'bg-blue-500 text-white',
},
},
});
// Render as a link
<Button color="primary" render={<a href="/home" />}>
Go Home
</Button>;
// Render with custom component
import { Link } from 'react-router-dom';
<Button color="primary" render={props => <Link {...props} to="/home" />}>
Go Home
</Button>;Props, refs, and event handlers are automatically merged!
When render is a function, its argument is intentionally broad and spread-safe: it includes generic HTMLAttributes<any>, a flattened className, an optional ref, and any variant props listed in forwardProps. Base-element-specific props like type, disabled, form, href, and target are intentionally not part of the typed/stable callback contract.
Note: The
renderprop pattern is a well-established composition pattern in the React ecosystem, used by libraries like Base UI and Ariakit for building accessible, composable components.
API Reference
defineConfig(options?)
Creates a configured factory for creating variants and components.
const config = defineConfig({
onClassesMerged?: (classNames: string) => string;
});Options:
onClassesMerged- Function to merge/process final class names (e.g.,twMerge)
Returns:
variants- Function to create class name resolversvariantComponent- Function to create React componentsvariantPropsResolver- Function to create props resolvers
defineVariantConfig(config)
Captures a reusable variants config with literal-preserving inference.
This is useful when you want to hoist a config into a shared constant and then
pass it to both variants() and variantComponent() without adding as const
to each nested value manually.
import { defineConfig, defineVariantConfig } from 'react-class-variants';
const { variants, variantComponent } = defineConfig();
const surfaceConfig = defineVariantConfig({
base: ['rounded-xl', 'p-4'],
variants: {
appearance: {
outlined: 'bg-white border',
soft: 'bg-gray-100 border',
},
interactive: {
true: 'cursor-pointer',
false: '',
},
},
defaultVariants: {
appearance: 'outlined',
interactive: false,
},
});
const surfaceVariants = variants(surfaceConfig);
const Surface = variantComponent('div', surfaceConfig);Top-level as const is also supported for reusable configs, including readonly
class arrays and readonly compoundVariants selector arrays.
variants(config)
Creates a function that resolves variant props to class names.
const buttonVariants = variants({
base?: ClassNameValue;
variants?: {
[variantName: string]: {
[variantValue: string]: ClassNameValue;
};
};
compoundVariants?: ReadonlyArray<{
variants: Record<string, string | readonly string[]>;
className: ClassNameValue;
}>;
defaultVariants?: Record<string, string>;
});Returns: (props) => string
variantComponent(element, config)
Creates a React component with variant support.
const Button = variantComponent(
element: string | React.ComponentType,
config: VariantsConfig & {
displayName?: string;
withoutRenderProp?: boolean;
forwardProps?: readonly string[];
}
);Config Options:
- All
VariantsConfigoptions (base,variants,compoundVariants,defaultVariants) displayName- Custom React DevTools display name for the generated component (optional)withoutRenderProp- Disables therenderprop pattern (optional)forwardProps- Array of variant prop names to keep in the resolved props object and expose torenderor custom targets (optional)
Component Props:
- All variant props (inferred from config)
- Native element props (e.g.,
onClick,disabled) className- Additional classes (merged with highest priority)render- Polymorphic rendering (unlesswithoutRenderPropis true). Function renders receive a broad spread-safe prop bag: genericHTMLAttributes<any>, a flattenedclassName: string, an optionalref, and any forwarded variant props. Base-element-specific props liketype,disabled,form,href, andtargetare intentionally not part of the typed/stable callback contract.
variantPropsResolver(config)
Creates a function that extracts variant props and resolves them to a className.
const resolveButtonProps = variantPropsResolver(config);
const { className, ...rest } = resolveButtonProps({
color: 'primary',
size: 'lg',
className: ['inline-flex', ['gap-2']],
onClick: handleClick,
});
// className: resolved variant classes as a flattened string
// rest: { onClick: handleClick }variantPropsResolver() accepts the same ClassNameValue shapes as variants() for its input className, but always returns a resolved className: string.
TypeScript
Full TypeScript support with automatic type inference.
Type Inference
const Button = variantComponent('button', {
variants: {
color: {
primary: 'bg-blue-500',
secondary: 'bg-gray-500',
},
size: {
sm: 'text-sm',
lg: 'text-lg',
},
},
defaultVariants: {
size: 'sm',
},
});
// TypeScript knows:
// ✅ color is required (no default)
// ✅ size is optional (has default)
// ✅ color only accepts 'primary' | 'secondary'
// ✅ size only accepts 'sm' | 'lg'
<Button color="primary" /> // ✅
<Button color="invalid" /> // ❌ Type error
<Button size="sm" /> // ❌ Type error (missing color)
<Button color="primary" size="lg" /> // ✅Reusable Configs
Inline configs usually infer well automatically. For hoisted reusable configs,
use defineVariantConfig() to preserve nested literals at the definition site:
import { defineConfig, defineVariantConfig } from 'react-class-variants';
const { variants, variantComponent } = defineConfig();
const badgeConfig = defineVariantConfig({
base: ['inline-flex', 'items-center'],
variants: {
tone: {
neutral: 'bg-slate-100 text-slate-900',
accent: 'bg-sky-500 text-white',
},
outlined: {
true: 'ring-1 ring-inset',
false: '',
},
},
compoundVariants: [
{
variants: {
tone: 'accent',
outlined: true,
},
className: 'ring-sky-300',
},
],
defaultVariants: {
tone: 'neutral',
outlined: false,
},
});
const badge = variants(badgeConfig);
const Badge = variantComponent('span', badgeConfig);If you prefer, a top-level as const now also works cleanly for reusable
configs, including readonly class arrays:
const badgeConfig = {
base: ['inline-flex', 'items-center'],
variants: {
tone: {
neutral: ['bg-slate-100', 'text-slate-900'],
accent: ['bg-sky-500', 'text-white'],
},
},
defaultVariants: {
tone: 'neutral',
},
} as const;Optional vs Required
Variants are required by default. They become optional when:
- They are boolean variants (
"true"/"false"keys) - They have a value in
defaultVariants
const component = variants({
variants: {
color: { red: '...', blue: '...' }, // Required
size: { sm: '...', lg: '...' }, // Required
outlined: { true: '...', false: '...' }, // Optional (boolean)
},
defaultVariants: {
size: 'sm', // Makes size optional
},
});
// color: required
// size: optional (has default)
// outlined: optional (boolean)Type Utilities
React Class Variants provides several utility types for working with variants and components:
import type {
VariantsConfig,
VariantOptions,
ClassNameValue,
ExtractVariantOptions,
ExtractVariantConfig,
} from 'react-class-variants';
// Extract config type
type Config = VariantsConfig<typeof myConfig>;
// Extract variant props
type Variants = VariantOptions<typeof myConfig>;
// Use in props
type Props = {
className?: ClassNameValue;
};ExtractVariantOptions<T>
Universal type utility that extracts variant options from any variant function, resolver, or component. Works with:
variants()- className resolver functionsvariantPropsResolver()- props resolver functionsvariantComponent()- React components
This is useful when you need to reference variant props in other parts of your code.
const { variants, variantPropsResolver, variantComponent } = defineConfig();
// Works with variants()
const buttonVariants = variants({
variants: {
color: {
primary: 'bg-blue-500',
secondary: 'bg-gray-500',
},
size: {
sm: 'text-sm',
lg: 'text-lg',
},
},
defaultVariants: {
size: 'sm',
},
});
type ButtonOptions1 = ExtractVariantOptions<typeof buttonVariants>;
// Result: { color: 'primary' | 'secondary', size?: 'sm' | 'lg' }
// Works with variantPropsResolver()
const resolveButtonProps = variantPropsResolver({
variants: {
variant: { solid: 'bg-fill', outline: 'border' },
},
});
type ButtonOptions2 = ExtractVariantOptions<typeof resolveButtonProps>;
// Result: { variant: 'solid' | 'outline' }
// Works with variantComponent()
const Button = variantComponent('button', {
variants: {
color: { primary: 'bg-blue-500' },
},
});
type ButtonOptions3 = ExtractVariantOptions<typeof Button>;
// Result: { color: 'primary' }
// Use in your own components
function ButtonGroup({ variant }: { variant: ButtonOptions1['color'] }) {
return (
<div>
<Button color={variant} />
<Button color={variant} />
</div>
);
}Key Points:
- Universal: Works with all three core functions (
variants,variantPropsResolver,variantComponent) - Respects optional vs required variants (based on
defaultVariantsand boolean variants) - Includes only variant props (excludes native element props like
onClick,className, etc.) - Useful for prop forwarding and composition
React Class Variants also exports runtime utilities from the package root:
import {
hasOwnProperty,
mergeProps,
mergeRefs,
useMergeRefs,
} from 'react-class-variants';ExtractVariantConfig<T>
Universal type utility that extracts the full configuration from any variant function, resolver, or component. Works with:
variants()- className resolver functionsvariantPropsResolver()- props resolver functionsvariantComponent()- React components
This is useful for reusing or extending configurations.
const { variants, variantPropsResolver, variantComponent } = defineConfig();
// Works with variants()
const buttonVariants = variants({
base: 'rounded font-semibold',
variants: {
color: {
primary: 'bg-blue-500',
secondary: 'bg-gray-500',
},
},
defaultVariants: {
color: 'primary',
},
compoundVariants: [
{
variants: { color: 'primary' },
className: 'shadow-lg',
},
],
});
type ButtonConfig1 = ExtractVariantConfig<typeof buttonVariants>;
// Result: {
// base?: ClassNameValue,
// variants?: { color: { primary: string, secondary: string } },
// defaultVariants?: { color: 'primary' | 'secondary' },
// compoundVariants?: Array<...>
// }
// Works with variantPropsResolver()
const resolveProps = variantPropsResolver({
base: 'input',
variants: { size: { sm: 'h-8', lg: 'h-12' } },
});
type ResolverConfig = ExtractVariantConfig<typeof resolveProps>;
// Works with variantComponent()
const Button = variantComponent('button', {
base: 'btn',
variants: { color: { primary: 'bg-blue' } },
});
type ButtonConfig2 = ExtractVariantConfig<typeof Button>;
// Reuse config with modifications
const dangerVariants = variants({
...(buttonVariants as any), // Note: need type assertion for runtime config access
variants: {
color: {
danger: 'bg-red-500 text-white',
warning: 'bg-yellow-500 text-black',
},
},
});Key Points:
- Universal: Works with all three core functions (
variants,variantPropsResolver,variantComponent) - Extracts the complete
VariantsConfigincludingbase,variants,defaultVariants, andcompoundVariants - Useful for creating derived components or sharing configurations
- Returns a prettified type for better IDE support
Usage with Different CSS Solutions
Tailwind CSS
import { defineConfig } from 'react-class-variants';
import { twMerge } from 'tailwind-merge';
const { variantComponent } = defineConfig({
onClassesMerged: twMerge,
});
const Button = variantComponent('button', {
base: 'rounded font-medium transition-colors',
variants: {
color: {
blue: 'bg-blue-500 hover:bg-blue-600 text-white',
red: 'bg-red-500 hover:bg-red-600 text-white',
},
},
});CSS Modules
import { defineConfig } from 'react-class-variants';
import styles from './Button.module.css';
const { variantComponent } = defineConfig();
const Button = variantComponent('button', {
base: styles.button,
variants: {
color: {
primary: styles.primary,
secondary: styles.secondary,
},
size: {
sm: styles.small,
lg: styles.large,
},
},
});Plain CSS
import { defineConfig } from 'react-class-variants';
import './Button.css';
const { variantComponent } = defineConfig();
const Button = variantComponent('button', {
base: 'btn',
variants: {
color: {
primary: 'btn-primary',
secondary: 'btn-secondary',
},
},
});Mixed Approaches
import { defineConfig } from 'react-class-variants';
import { twMerge } from 'tailwind-merge';
import styles from './Button.module.css';
const { variantComponent } = defineConfig({
onClassesMerged: twMerge,
});
const Button = variantComponent('button', {
base: [styles.button, 'transition-all'],
variants: {
color: {
primary: [styles.primary, 'shadow-lg'],
secondary: [styles.secondary, 'shadow-md'],
},
},
});Real-World Examples
Button Component
import { defineConfig } from 'react-class-variants';
import { twMerge } from 'tailwind-merge';
const { variantComponent } = defineConfig({ onClassesMerged: twMerge });
export const Button = variantComponent('button', {
base: [
'inline-flex items-center justify-center',
'font-medium rounded-lg',
'transition-all duration-200',
'focus:outline-none focus:ring-2 focus:ring-offset-2',
'disabled:opacity-50 disabled:cursor-not-allowed',
],
variants: {
variant: {
solid: '',
outline: 'bg-transparent border-2',
ghost: 'bg-transparent',
},
color: {
blue: '',
red: '',
green: '',
gray: '',
},
size: {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
},
},
compoundVariants: [
// Solid variants
{
variants: { variant: 'solid', color: 'blue' },
className: 'bg-blue-600 hover:bg-blue-700 text-white focus:ring-blue-500',
},
{
variants: { variant: 'solid', color: 'red' },
className: 'bg-red-600 hover:bg-red-700 text-white focus:ring-red-500',
},
{
variants: { variant: 'solid', color: 'green' },
className:
'bg-green-600 hover:bg-green-700 text-white focus:ring-green-500',
},
{
variants: { variant: 'solid', color: 'gray' },
className: 'bg-gray-600 hover:bg-gray-700 text-white focus:ring-gray-500',
},
// Outline variants
{
variants: { variant: 'outline', color: 'blue' },
className:
'border-blue-600 text-blue-600 hover:bg-blue-50 focus:ring-blue-500',
},
{
variants: { variant: 'outline', color: 'red' },
className:
'border-red-600 text-red-600 hover:bg-red-50 focus:ring-red-500',
},
// Ghost variants
{
variants: { variant: 'ghost', color: 'blue' },
className: 'text-blue-600 hover:bg-blue-50 focus:ring-blue-500',
},
],
defaultVariants: {
variant: 'solid',
color: 'blue',
size: 'md',
},
});Usage:
<Button>Default</Button>
<Button variant="outline" color="red" size="lg">Outline</Button>
<Button variant="ghost" color="green">Ghost</Button>
<Button disabled>Disabled</Button>
<Button render={<a href="/" />}>Link Button</Button>Card Component
export const Card = variantComponent('div', {
base: 'rounded-lg overflow-hidden',
variants: {
variant: {
elevated: 'shadow-md hover:shadow-lg transition-shadow',
outlined: 'border border-gray-200',
filled: 'bg-gray-50',
},
padding: {
none: 'p-0',
sm: 'p-4',
md: 'p-6',
lg: 'p-8',
},
},
defaultVariants: {
variant: 'elevated',
padding: 'md',
},
});
export const CardHeader = variantComponent('div', {
base: 'border-b border-gray-200 pb-4 mb-4',
});
export const CardTitle = variantComponent('h3', {
base: 'text-lg font-semibold text-gray-900',
});
export const CardContent = variantComponent('div', {
base: 'text-gray-600',
});Usage:
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
</CardHeader>
<CardContent>Card content goes here</CardContent>
</Card>Badge Component
export const Badge = variantComponent('span', {
base: 'inline-flex items-center font-medium rounded-full',
variants: {
variant: {
solid: '',
outline: 'border bg-transparent',
subtle: '',
},
color: {
gray: '',
blue: '',
green: '',
yellow: '',
red: '',
},
size: {
sm: 'px-2 py-0.5 text-xs',
md: 'px-2.5 py-0.5 text-sm',
lg: 'px-3 py-1 text-base',
},
},
compoundVariants: [
// Solid
{
variants: { variant: 'solid', color: 'gray' },
className: 'bg-gray-100 text-gray-800',
},
{
variants: { variant: 'solid', color: 'blue' },
className: 'bg-blue-100 text-blue-800',
},
{
variants: { variant: 'solid', color: 'green' },
className: 'bg-green-100 text-green-800',
},
{
variants: { variant: 'solid', color: 'yellow' },
className: 'bg-yellow-100 text-yellow-800',
},
{
variants: { variant: 'solid', color: 'red' },
className: 'bg-red-100 text-red-800',
},
// Outline
{
variants: { variant: 'outline', color: 'blue' },
className: 'border-blue-500 text-blue-700',
},
// Subtle
{
variants: { variant: 'subtle', color: 'blue' },
className: 'bg-blue-50 text-blue-700',
},
],
defaultVariants: {
variant: 'solid',
color: 'gray',
size: 'md',
},
});Input Component
export const Input = variantComponent('input', {
base: [
'w-full rounded-md border transition-colors',
'focus:outline-none focus:ring-2 focus:ring-offset-1',
'disabled:opacity-50 disabled:cursor-not-allowed',
],
variants: {
variant: {
outline:
'bg-white border-gray-300 focus:border-blue-500 focus:ring-blue-200',
filled:
'bg-gray-100 border-transparent focus:bg-white focus:ring-blue-200',
flushed:
'bg-transparent border-t-0 border-x-0 border-b-2 rounded-none focus:ring-0',
},
size: {
sm: 'px-2 py-1.5 text-sm',
md: 'px-3 py-2 text-base',
lg: 'px-4 py-3 text-lg',
},
error: {
true: 'border-red-500 focus:border-red-500 focus:ring-red-200',
},
},
defaultVariants: {
variant: 'outline',
size: 'md',
},
});Advanced Patterns
Sharing Configurations
// config/variants.ts
import { defineConfig } from 'react-class-variants';
import { twMerge } from 'tailwind-merge';
export const { variants, variantComponent } = defineConfig({
onClassesMerged: twMerge,
});
// components/Button.tsx
import { variantComponent } from '@/config/variants';
export const Button = variantComponent('button', { ... });
// components/Card.tsx
import { variantComponent } from '@/config/variants';
export const Card = variantComponent('div', { ... });Extending Components
const BaseButton = variantComponent('button', {
base: 'rounded font-medium',
variants: {
size: {
sm: 'px-3 py-1',
lg: 'px-6 py-3',
},
},
});
// Extend with additional props
const IconButton = ({
icon,
children,
...props
}: React.ComponentProps<typeof BaseButton> & { icon: React.ReactNode }) => {
return (
<BaseButton {...props}>
{icon}
{children}
</BaseButton>
);
};Dynamic Variants
const createColorVariants = (colors: string[]) => {
return colors.reduce((acc, color) => {
acc[color] = `bg-${color}-500 text-white hover:bg-${color}-600`;
return acc;
}, {} as Record<string, string>);
};
const Button = variantComponent('button', {
variants: {
color: createColorVariants(['blue', 'red', 'green', 'purple']),
},
});Forwarding Variant Props
By default, variant props are consumed and removed from the resolved props object. Use forwardProps to keep specific variant props available for valid DOM props, custom components, or render functions:
const Button = variantComponent('button', {
base: 'px-4 py-2 rounded font-medium transition-colors',
variants: {
color: {
primary: 'bg-blue-600 hover:bg-blue-700 text-white',
secondary: 'bg-gray-600 hover:bg-gray-700 text-white',
},
disabled: {
true: 'opacity-50 cursor-not-allowed',
false: '',
},
},
// Keep 'disabled' in the resolved props object. Since <button> accepts it,
// React will also reflect it to the DOM.
forwardProps: ['disabled'],
});
// The 'color' prop is consumed and removed from the resolved props object
// The 'disabled' prop is used for styling and remains available to <button>
<Button color="primary" disabled>
Submit
</Button>;forwardProps does not force React to render arbitrary unknown props on native DOM elements. It keeps the selected variant props in the resolved props object; native DOM reflection only happens when the rendered target actually accepts that prop.
For non-DOM variant keys, map them explicitly in render or a custom component:
const Button = variantComponent('button', {
variants: {
size: {
sm: 'text-sm',
lg: 'text-lg',
},
},
forwardProps: ['size'],
});
<Button size="lg" render={props => <div {...props} data-size={props.size} />}>
Custom target
</Button>;Performance
React Class Variants is optimized for performance:
- Zero Runtime Dependencies - Only peer dependency is React
- Minimal Bundle Size - ~2KB minified + gzipped
- Efficient Caching - Boolean variant lookups are cached
- No Re-renders - Components only re-render when props change
- Tree-Shakeable - Import only what you use
Comparison
| | react-class-variants | CVA | classname-variants | tailwind-variants | Stitches |
| ----------------- | :-----------------------------: | :------: | :-------------------------: | :---------------: | --------- |
| Framework | React | Agnostic | React | Agnostic | React |
| TypeScript | ✅ | ✅ | ✅ | ✅ | ✅ |
| Variants | ✅ | ✅ | ✅ | ✅ | ✅ |
| Compound Variants | ✅ | ✅ | ✅ | ✅ | ✅ |
| React Components | ✅ Built-in | ❌ | ✅ Built-in | ❌ | ✅ |
| Polymorphic | ✅ Built-in (via render prop) | ❌ | ✅ Built-in (via as prop) | ❌ | ✅ |
| Forward Props | ✅ forwardProps | ❌ | ✅ forwardProps | ❌ | ❌ |
| CSS Solution | Any | Any | Any | Tailwind | CSS-in-JS |
Contributing
Contributions are welcome! Please check out our Contributing Guide.
# Clone the repo
git clone https://github.com/Jackardios/react-class-variants.git
# Install dependencies
pnpm install
# Type-check publish surface
pnpm lint
# Type-check src plus runtime tests (excluding tsd files)
pnpm lint:all
# Run the reusable verification gate
pnpm run verify
# Run tests in watch mode
pnpm dev
# Build
pnpm build
# Run release-intent checks
pnpm run cipnpm run verify runs the reusable package gate: lint, lint:all, ESLint, Prettier, runtime tests, type tests, and package linting with publint.
pnpm run ci adds the release-intent changeset check on top of verify.
Release Process
The alpha line is maintained from the next branch.
- Open all v2 feature and fix PRs against
next - Add a changeset for any source, package metadata, public type, or build/release-affecting change
- Use
pnpm run verifyfor the reusable package gate - Use
pnpm run check:changesetorpnpm run cito validate release intent on the current branch - Alpha publishes are triggered from
nextvia npm trusted publishing - After each alpha publish, run the manual npm
dist-tagand legacydeprecatecommands from the release process doc - When the package is ready for stable, run
changeset pre exit, publish2.0.0, and then fast-forwardmainto the stable release commit
License
MIT © Salavat Salakhutdinov
