TypeScript
Extract typed props with VariantProps, work with slotted return types, and keep variant inference reliable.
Tailwind Variants is written in TypeScript. Variant keys and values infer automatically — no code generation required.
VariantProps
Extract the props a recipe accepts:
import { tv, type VariantProps } from 'tailwind-variants';
export const button = tv({
base: 'inline-flex cursor-pointer items-center justify-center rounded-full px-4 py-1.5 font-medium select-none',
variants: {
variant: {
primary: 'bg-zinc-900 text-white',
secondary: 'bg-zinc-100 text-zinc-900',
tertiary: 'text-zinc-600'
},
flat: {
true: 'bg-transparent shadow-none'
}
},
defaultVariants: {
variant: 'primary'
}
});
type ButtonVariants = VariantProps<typeof button>;
// variant?: "primary" | "secondary" | "tertiary"
// flat?: boolean
interface ButtonProps extends ButtonVariants {
children: React.ReactNode;
className?: string;
}
export function Button({ children, className, ...variants }: ButtonProps) {
return (
<button className={button({ ...variants, className })}>
{children}
</button>
);
}Keys with defaultVariants become optional on the type.
Required variants
TV does not have a built-in "required variant" flag. Use TypeScript utilities:
type ButtonVariants = VariantProps<typeof button>;
type RequiredSize = Omit<ButtonVariants, 'size'> &
Required<Pick<ButtonVariants, 'size'>>;Or model the axis without a default so TypeScript keeps it required.
Slotted return types
Slotted recipes return an object of slot functions. Destructure once for cleaner types:
const alert = tv({
slots: {
base: 'flex gap-3 rounded-lg p-4',
title: 'font-semibold',
description: 'text-sm'
},
variants: {
color: {
default: {
base: 'bg-zinc-100',
title: 'text-zinc-900'
},
danger: {
base: 'bg-red-50',
title: 'text-red-900'
}
}
}
});
type AlertSlots = ReturnType<typeof alert>;
// AlertSlots.base, .title, .description — each (props?) => string
function Alert({ color }: VariantProps<typeof alert>) {
const { base, title, description } = alert({ color });
return (
<div className={base()}>
<p className={title()}>Title</p>
<p className={description()}>Body</p>
</div>
);
}as const for external definitions
When variants live outside tv, use as const so TypeScript preserves literal keys:
const variants = {
primary: 'bg-zinc-900 text-white',
secondary: 'bg-zinc-100 text-zinc-900',
tertiary: 'text-zinc-600'
} as const;
const button = tv({
variants: { variant: variants }
});