TypeScript

Infer variant props with VariantProps, tighten required keys, and type slotted return values.

UnoCSS Variants is written in TypeScript. Variant keys and values infer from the object you pass to uv — no codegen.

VariantProps

Extract the props a recipe accepts. class and className are omitted so you can add your own host-prop type.

import type { VariantProps } from 'unocss-variants';
import { uv } from 'unocss-variants';

export const button = uv({
  base: 'px-4 py-1.5 rounded-full hover:opacity-80',
  variants: {
    color: {
      primary: 'bg-blue-500 color-white',
      neutral: 'bg-zinc-500 color-black dark:color-white'
    },
    flat: {
      true: 'bg-transparent'
    }
  },
  defaultVariants: {
    color: 'primary'
  },
  compoundVariants: [
    {
      color: 'primary',
      flat: true,
      class: 'bg-blue-500/40'
    },
    {
      color: 'neutral',
      flat: true,
      class: 'bg-zinc-500/20'
    }
  ]
});

type ButtonVariants = VariantProps<typeof button>;
// {
//   color?: 'primary' | 'neutral'
//   flat?: boolean
// }

Keys present in defaultVariants stay optional. Boolean axes (true / false keys) infer as boolean.

In Vue, extend the type for your component props:

interface ButtonProps extends ButtonVariants {
  class?: string;
}

Required variants

There is no required: true flag. Use TypeScript utilities:

import type { VariantProps } from 'unocss-variants';
import { uv } from 'unocss-variants';

const buttonStyles = uv({
  base: 'px-4 py-1.5 rounded-full hover:opacity-80',
  variants: {
    color: {
      primary: 'bg-blue-500 color-white',
      neutral: 'bg-zinc-500 color-black dark:color-white'
    },
    requiredFlat: {
      true: 'bg-transparent',
      false: 'bg-white'
    }
  },
  defaultVariants: {
    color: 'primary'
  }
});

type ButtonVariants = VariantProps<typeof buttonStyles>;

export interface ButtonProps
  extends Omit<ButtonVariants, 'requiredFlat'>,
  Required<Pick<ButtonVariants, 'requiredFlat'>> {}

export function button(props: ButtonProps) {
  return buttonStyles(props);
}

// @ts-expect-error missing requiredFlat
button({});

button({ requiredFlat: true });

Leaving an axis out of defaultVariants also keeps it required in practice at the type level if you model it that way — VariantProps still marks every variant key optional because the runtime accepts a missing key (it just skips that axis). Use Required<Pick<...>> when the UI must not render without a value.

Slotted return types

A recipe with slots returns an object of functions. ReturnType<typeof recipe> is that object:

const alert = uv({
  slots: {
    base: 'flex gap-3 rounded-lg p-4',
    title: 'font-semibold',
    description: 'text-sm'
  },
  variants: {
    color: {
      default: {
        base: 'bg-zinc-100',
        title: 'color-zinc-900'
      },
      danger: {
        base: 'bg-red-50',
        title: 'color-red-900'
      }
    }
  }
});

type AlertSlots = ReturnType<typeof alert>;
// { base: (props?) => string; title: (props?) => string; description: (props?) => string }

function classesFor(color: VariantProps<typeof alert>['color']) {
  const { base, title, description } = alert({ color });
  return { base: base(), title: title(), description: description() };
}

The exported return type name is UvReturnType (not UVReturnType). It is a callable plus metadata: base, slots, variants, variantKeys, defaultVariants, compoundVariants, compoundSlots, and extend.

as const for external maps

When variant maps live outside uv, use as const so keys stay literal:

const colors = {
  primary: 'bg-zinc-900 color-white',
  secondary: 'bg-zinc-100 color-zinc-900'
} as const;

const button = uv({
  variants: {
    color: colors
  }
});