
Security News
Axios Supply Chain Attack Reaches OpenAI macOS Signing Pipeline, Forces Certificate Rotation
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.
react-class-variants
Advanced tools
Type-safe React variants API for dynamic CSS class composition
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.
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.
tailwind-merge or custom functionreact-tailwind-variants -> react-class-variantsalphanpm install react-class-variants@alphaCompatibility:
19+20.19+react-class-variants@alphanpm install react-class-variants@alpha
yarn add react-class-variants@alpha
pnpm add react-class-variants@alpha
Optional: For Tailwind CSS class conflict resolution:
npm install tailwind-merge
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>;
}
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>
);
}
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" />;
If you're using Tailwind CSS, you can enable autocompletion and syntax highlighting for class names inside your variant configurations.
Install the Tailwind CSS IntelliSense extension for VS Code
Add the following configuration to your VS Code settings.json:
{
"tailwindCSS.classFunctions": [
"variants",
"variantPropsResolver",
"variantComponent"
]
}
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:
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 classes
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: true
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',
},
];
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' });
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.
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 resolversdefineVariantConfig(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:
VariantsConfig options (base, variants, compoundVariants, defaultVariants)displayName - Custom React DevTools display name for the generated component (optional)withoutRenderProp - Disables the render prop pattern (optional)forwardProps - Array of variant prop names to keep in the resolved props object and expose to render or custom targets (optional)Component Props:
onClick, disabled)className - Additional classes (merged with highest priority)render - Polymorphic rendering (unless withoutRenderProp is true). Function renders receive a broad spread-safe prop bag: generic HTMLAttributes<any>, a flattened className: string, an optional ref, and any forwarded variant props. Base-element-specific props like type, disabled, form, href, and target are 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.
Full TypeScript support with automatic 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" /> // ✅
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;
Variants are required by default. They become optional when:
"true" / "false" keys)defaultVariantsconst 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)
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 componentsThis 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:
variants, variantPropsResolver, variantComponent)defaultVariants and boolean variants)onClick, className, etc.)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 componentsThis 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:
variants, variantPropsResolver, variantComponent)VariantsConfig including base, variants, defaultVariants, and compoundVariantsimport { 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',
},
},
});
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,
},
},
});
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',
},
},
});
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'],
},
},
});
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>
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>
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',
},
});
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',
},
});
// 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', { ... });
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>
);
};
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']),
},
});
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>;
React Class Variants is optimized for performance:
| 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 |
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 ci
pnpm 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.
The alpha line is maintained from the next branch.
nextpnpm run verify for the reusable package gatepnpm run check:changeset or pnpm run ci to validate release intent on the current branchnext via npm trusted publishingdist-tag and legacy deprecate commands from the release process docchangeset pre exit, publish 2.0.0, and then fast-forward main to the stable release commitMIT © Salavat Salakhutdinov
FAQs
Type-safe React variants API for dynamic CSS class composition
The npm package react-class-variants receives a total of 9 weekly downloads. As such, react-class-variants popularity was classified as not popular.
We found that react-class-variants demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.

Security News
Open source is under attack because of how much value it creates. It has been the foundation of every major software innovation for the last three decades. This is not the time to walk away from it.

Security News
Socket CEO Feross Aboukhadijeh breaks down how North Korea hijacked Axios and what it means for the future of software supply chain security.