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.
Numeric keys
Options keyed by numbers, such as spacing scales, stay number literals on the type:
const box = tv({
variants: {
gap: { px: 'gap-px', 0: 'gap-0', 2: 'gap-2' },
},
});
type BoxVariants = VariantProps<typeof box>;
// gap?: "px" | 0 | 2
box({ gap: 2 }); // "gap-2"Pass them as numbers. String forms such as "2" are not part of 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.
Stricter validation
Unknown or misspelled keys in variants, compoundVariants, and defaultVariants fail at compile time, as do unknown slot names in slot-shaped class values. Nothing gets silently ignored:
const button = tv({
variants: { size: { sm: 'text-sm' } },
// ✗ compile error — 'colour' is not a variant key
defaultVariants: { colour: 'primary' }
});If an upgrade surfaces new type errors here, they almost always point at real typos. See Migration.
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 }
});