Vize

Typed Class Variants

Headless Typed Class Variants; covers class-variance-authority, tailwind-variants, Panda CSS recipes.

Package @vizejs/ui/variants
Maturity stable
Own the source vize lib pull variants
Requires —
Aliases cva, class variance authority, tailwind variants, recipe, cx, clsx
Covers class-variance-authority, tailwind-variants, Panda CSS recipes, clsx

API

cx

Join class values into one space-separated string. Accepts strings, numbers, nested arrays, and dictionaries whose truthy keys are included. Falsy values are skipped. Dependency-free and pure, so it is safe during SSR and produces identical output on server and client.

function cx(...inputs: readonly ClassValue[]): string

createCx

Create a cx variant that post-processes its output, e.g. with tailwind-merge: ts import { twMerge } from "tailwind-merge"; export const cn = createCx(twMerge);

function createCx(merge: ClassMerger): (...inputs: readonly ClassValue[]) => string

defineVariants

Define a typed class recipe (a cva/tailwind-variants style API). Variant props are inferred from the configuration: option keys become a string-literal union, true/false keys become boolean, and defaultVariants/compoundVariants are checked against them. With responsive, each variant also accepts { initial, sm, md, ... } maps whose classes are prefixed with the breakpoint. Pure and allocation-light, so it renders identically during SSR.

function defineVariants< const Variants extends VariantDefinitions = {}, const Breakpoint extends string = never, >( config: VariantsConfig<Variants, {}, Breakpoint> & { readonly slots?: undefined }, options?: DefineVariantsOptions, ): ClassRecipe<Variants, Breakpoint>

Behavior

Normative state x input -> outcome table for @vizejs/ui/variants. Every row is proven by src/families/foundations/variants/variants.test.ts; compile-only inference assertions live in src/families/foundations/variants/variants.types.test-d.ts.

# State Input Outcome Proven by
VR1 any class values cx(...) strings/numbers/bigints joined in order; nested lists flattened; truthy dictionary keys kept cx joins strings, numbers, nested lists, and truthy dictionary keys
VR2 merge hook createCx(merge)(...) joined output is passed through the hook exactly once createCx applies the merge hook to the joined output
VR3 class recipe call with and without props base, defaults, variant options, matching compounds, then class, in that order recipes apply base, defaults, variants, compounds, then class in order
VR4 unknown option / unset runtime props outside the types unknown options are ignored; an unset variant with a false option uses it unknown runtime options are ignored and undefined boolean variants use false
VR5 recipe merge option call the configured merger post-processes recipe output merge hook post-processes recipe output
VR6 slot recipe call, then slot functions one function per slot (base first); per-slot options/compounds; slot overrides append slot recipes return per-slot class functions with overrides
VR7 responsive breakpoints { initial, md } value initial classes unprefixed, others prefixed per token; unknown breakpoints throw VIZE_UI_VARIANTS_BREAKPOINT responsive values prefix classes per breakpoint and reject unknown breakpoints
VR8 forwarding splitVariantProps(props) variant props and the remaining props are separated splitVariantProps separates variant props from forwarded props
VR9 server render render twice, then hydrate byte-identical markup and no hydration diagnostics renders identical classes on the server, hydrates server markup without mismatch diagnostics
VR10 public type API props, defaults, compounds, slots option unions, boolean variants, slot keys, and breakpoints are inferred; typos fail to compile src/families/foundations/variants/variants.types.test-d.ts

Recipes are pure functions of their input: no DOM, globals, or lifecycle, so they are identical on the server, in Vapor components, and in plain scripts.