Class Variance AuthorityLoading saved progress…

Class Variance Authority

Component libraries written with utility CSS (like Tailwind) end up with a button that needs different classes for each intent and each size. class-variance-authority (cva) is the popular helper that turns those choices into a clean className string. You'll implement a simplified version: classVarianceAuthority(config) takes a config object and returns a function that maps a set of props to a single space-joined string of class names.

Signature

type VariantConfig = {
  base?: string;                                  // classes always applied
  variants?: Record<string, Record<string, string>>; // variantName -> value -> classes
  defaultVariants?: Record<string, string>;       // variantName -> value used when prop is absent
};

// returns a function from selected props to a className string
function classVarianceAuthority(
  config: VariantConfig,
): (props?: Record<string, string>) => string;

For each variant key, the chosen value is props[key] when the caller supplied it, otherwise defaultVariants[key]. Look that value up in variants[key] to get its class string.

Examples

const button = classVarianceAuthority({
  base: 'btn',
  variants: {
    intent: { primary: 'bg-blue-500', secondary: 'bg-gray-500' },
    size: { sm: 'text-sm', lg: 'text-lg' },
  },
  defaultVariants: { intent: 'primary', size: 'sm' },
});

button({ intent: 'secondary', size: 'lg' }); // 'btn bg-gray-500 text-lg'
button({ size: 'lg' });                       // 'btn bg-blue-500 text-lg'  (intent default)
button();                                     // 'btn bg-blue-500 text-sm'  (all defaults)
// An unknown variant value contributes nothing — and does NOT fall back to the default.
button({ intent: 'danger' }); // 'btn text-sm'  (intent adds nothing; size uses its default)

Notes

  • An absent prop uses defaultVariants; a present-but-unknown value does not. Passing intent: 'danger' (not in the table) adds no class and never reaches the default — those are two different cases.
  • Unknown values contribute nothing. No error, no fallback — the variant simply adds zero classes.
  • The output is clean. A single space between classes, no leading or trailing space, no doubled spaces — even when base is empty or every variant resolves to nothing.
  • Order follows the variants object. Emit classes in the order the keys are declared in variants, with base first.
  • Don't worry about compound variants, tailwind-merge style conflict resolution, boolean variants, or array/object class values — every class entry is a plain string. Those are out of scope (see the solution's Going further).
Loading editor…