Pair Tailwind with class-variance-authority (CVA) to create discoverable, type-safe variant APIs—no more stringly-typed class soups.
Core Pattern
import { cva, type VariantProps } from 'class-variance-authority';
const button = cva('inline-flex items-center justify-center rounded-md font-medium', {
variants: {
variant: { primary: 'bg-blue-600 text-white', ghost: 'hover:bg-gray-100' },
size: { sm: 'h-8 px-3', md: 'h-10 px-4' },
},
compoundVariants: [{ variant: 'primary', size: 'md', class: 'shadow' }],
defaultVariants: { variant: 'primary', size: 'md' },
});
export type ButtonVariants = VariantProps<typeof button>;
export function Button({
variant,
size,
className,
...rest
}: ButtonVariants & React.ButtonHTMLAttributes<HTMLButtonElement>) {
return <button className={button({ variant, size, className })} {...rest} />;
}Slots and Polymorphism
Use CVA per slot and a polymorphic as prop with constrained generics to keep refs and props typed.
import { forwardRef, ElementType, ComponentPropsWithRef } from 'react';
const card = cva('rounded-lg border bg-white', {
variants: { elevated: { true: 'shadow-md', false: '' } },
defaultVariants: { elevated: true },
});
type CardVariants = VariantProps<typeof card>;
type PolymorphicProps<C extends ElementType, P> = P & {
as?: C;
} & Omit<ComponentPropsWithRef<C>, keyof P | 'as'>;
const Card = forwardRef(
<C extends ElementType = 'div'>(
{ as, elevated, className, ...rest }: PolymorphicProps<C, CardVariants>,
ref: React.Ref<Element>,
) => {
const Comp = (as ?? 'div') as C;
return <Comp ref={ref as any} className={card({ elevated, className })} {...rest} />;
},
);Compound Variants and Strictness
Define mutually exclusive or combined states with compile‑time safety.
const badge = cva('inline-flex items-center rounded px-2 py-0.5 text-xs', {
variants: {
color: { gray: 'bg-gray-100 text-gray-800', red: 'bg-red-100 text-red-800' },
tone: { solid: '', outline: 'bg-transparent ring-1' },
},
compoundVariants: [{ color: 'red', tone: 'outline', class: 'ring-red-300' }],
defaultVariants: { color: 'gray', tone: 'solid' },
});Slot Classes Pattern
Use a record of CVAs for multi‑part components (e.g., input, label, helper).
const inputSlots = {
root: cva('grid gap-1'),
label: cva('text-sm font-medium'),
input: cva('rounded border px-3 py-2 focus:outline-none focus:ring'),
helper: cva('text-xs text-gray-500'),
};
type InputProps = {
label?: string;
helper?: string;
} & React.InputHTMLAttributes<HTMLInputElement>;
function Input({ label, helper, className, ...rest }: InputProps) {
return (
<div className={inputSlots.root()}>
{label && <label className={inputSlots.label()}>{label}</label>}
<input className={inputSlots.input({ className })} {...rest} />
{helper && <p className={inputSlots.helper()}>{helper}</p>}
</div>
);
}