search ESC

Searching…

No results for "".

Type at least 2 characters to search.

Docs

WindRecipe

A tv() (tailwind-variants) equivalent for Wind. Composes a className from a base, a set of variants (one axis per key, each mapping a value to a className), optional compoundVariants (multi-condition rules), and defaultVariants (the selection applied when an axis is not passed by the caller).

WindSlotRecipe extends the same model to multi-part components: instead of a single string, each call returns a Map of slot-name to className.

Table of Contents

Basic Usage

final button = WindRecipe(
  base: 'inline-flex items-center rounded-lg font-medium',
  variants: {
    'intent': {
      'primary': 'bg-blue-600 dark:bg-blue-500 text-white',
      'ghost':   'bg-transparent text-blue-600 dark:text-blue-400',
    },
    'size': {
      'sm': 'px-3 py-1.5 text-sm',
      'md': 'px-4 py-2 text-base',
    },
  },
  defaultVariants: {'intent': 'primary', 'size': 'md'},
);

// Full defaults: "inline-flex items-center rounded-lg font-medium bg-blue-600 dark:bg-blue-500 text-white px-4 py-2 text-base"
button()

// Override intent, keep size default:
button(variants: {'intent': 'ghost'})

// Append extra classes:
button(className: 'w-full')

Props: WindRecipe

Prop Type Default Description
base String '' Always-applied className.
variants Map> {} Variant axes: axis -> (value -> className). Iteration order fixes emission order.
compoundVariants List [] Multi-condition rules applied in list order after variant classes.
defaultVariants Map {} Default selection per axis. Overridden by the variants: argument at call site; a null value clears the default.

Call signature (WindRecipe is callable):

Param Type Default Description
variants Map? null Caller's axis selections. null for an axis clears its default (no class emitted). Unknown axis throws ArgumentError.
className String? null Extra caller className appended last.

Props: WindSlotRecipe

Prop Type Default Description
slots Map {} Base className per slot. Slot identity comes from these keys.
variants Map>> {} Variant axes: axis -> (value -> (slot -> className)).
compoundVariants List [] Multi-condition rules applied per slot in list order.
defaultVariants Map {} Default selection per axis.

Call signature (WindSlotRecipe is callable):

Param Type Default Description
variants Map? null Caller's axis selections. Unknown axis throws ArgumentError.
classNames Map? null Extra caller className per slot, appended last for that slot.

Constructor: WindRecipe

WindRecipe({
  String base = '',
  Map> variants = const {},
  List compoundVariants = const [],
  Map defaultVariants = const {},
})

WindCompoundVariant constructor:

WindCompoundVariant({
  required Map conditions,
  required String className,
})

A condition value is a String (exact match) or List (OR match).

Constructor: WindSlotRecipe

WindSlotRecipe({
  Map slots = const {},
  Map>> variants = const {},
  List compoundVariants = const [],
  Map defaultVariants = const {},
})

WindSlotCompoundVariant constructor:

WindSlotCompoundVariant({
  required Map conditions,
  required Map classNames,
})

Resolution Order

Every call resolves in this strict order: no dedupe, no sort, no twMerge:

base ++ variant-classes(definition order) ++ matched-compound(array order) ++ caller

The caller's className / classNames always arrive last, so they override earlier tokens via Wind's per-family last-wins rule.

A null value in variants: clears the default for that axis (no class emitted for it), matching tv()'s undefined-clears semantics.

The Caller-Append Contract (No twMerge)

WindRecipe and WindSlotRecipe only APPEND the caller's className / classNames; they never dedupe, sort, or resolve a conflict themselves. A caller passing a className that conflicts with the base (e.g. WindRecipe(base: 'w-1/2')(className: 'w-full')) gets both tokens in the emitted string, in that order: 'w-1/2 w-full'.

The conflict is resolved one layer down, when the string reaches WindParser: WindParser.findAndGroupClasses groups same-family classes together WITHOUT deduping, and each parser (e.g. SizingParser.parse) resolves them with last-class-wins. So the appended w-full wins over the base w-1/2 at parse time, not at recipe-call time. There is intentionally no Dart port of tailwind-merge: wind's parse-time per-family last-wins already achieves the same outcome for every token family the parsers understand, which is why WindRecipe deliberately only appends (see the no-twMerge rationale in the WindRecipe doc comment in lib/src/recipe/wind_recipe.dart).

This is verified by test/recipe/wind_recipe_test.dart (proves the append, order preserved) and test/parser/parsers/sizing_parser_test.dart (proves the appended w-full resolves to the winning style).

Note: the className-REPLACE anti-pattern (a component treating an incoming className as a full recipe bypass instead of appending it) is a consumer-component bug, not a wind defect; wind's own recipes always append. When it appears, it lives in a downstream consumer's component layer, outside this package.

Same-Granularity Override Contract

Because Wind's parser resolves conflicts per-family with last-class-wins at parse time, a variant must override a base token at the same granularity. Mixing a shorthand and a longhand of the same family silently keeps the shorthand:

// Correct: override px-* with px-*
WindRecipe(
  base: 'px-4',
  variants: {'size': {'sm': 'px-2'}},  // overrides px-4 correctly
)

// Incorrect: base p-* vs variant px-* (same family, different granularity)
WindRecipe(
  base: 'p-4',
  variants: {'size': {'sm': 'px-2'}},  // px-2 does NOT override p-4; p-4 wins silently
)

In debug builds, WindRecipe logs a warning the first time it detects a shorthand/longhand mix.

Styling Examples

Button with Compound Variants

Compound variants contribute a className only when every condition is satisfied:

final button = WindRecipe(
  base: 'inline-flex items-center rounded-lg font-medium',
  variants: {
    'intent': {
      'primary': 'bg-blue-600 dark:bg-blue-500 text-white',
      'destructive': 'bg-red-600 dark:bg-red-500 text-white',
    },
    'size': {'sm': 'px-3 py-1.5 text-sm', 'lg': 'px-6 py-3 text-lg'},
  },
  compoundVariants: [
    WindCompoundVariant(
      // Activates when intent is 'primary' OR 'destructive', AND size is 'lg'.
      conditions: {'intent': ['primary', 'destructive'], 'size': 'lg'},
      className: 'shadow-lg',
    ),
  ],
  defaultVariants: {'intent': 'primary', 'size': 'sm'},
);

Multi-Slot Card Recipe

final card = WindSlotRecipe(
  slots: {
    'root':   'rounded-xl border overflow-hidden',
    'header': 'px-5 py-4 border-b',
    'body':   'px-5 py-4',
  },
  variants: {
    'tone': {
      'default': {
        'root':   'bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700',
        'header': 'border-gray-200 dark:border-gray-700',
        'body':   '',
      },
      'highlighted': {
        'root':   'bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800',
        'header': 'border-blue-200 dark:border-blue-800',
        'body':   '',
      },
    },
  },
  defaultVariants: {'tone': 'default'},
);

final classes = card(variants: {'tone': 'highlighted'});
WCard(
  className: classes['root'],
  header: WDiv(className: classes['header'], child: WText('Summary')),
  child:  WDiv(className: classes['body'],   child: WText('Body')),
)

  • WBadge: a natural fit for a WindRecipe badge tone axis.
  • WCard: pairs with WindSlotRecipe for multi-slot card variants.
  • WButton: commonly driven by a WindRecipe for intent + size axes.