refract v0.1 · beta
Core concepts / Recipe variants

Recipe variants

Recipes come in three tiers: a recipe group (solid) is a family; a recipe (brand) is a named bundle of refs + css that resolves to a class; a variant is an optional modifier delta layered on a recipe. A recipe's variants map desugars to flat sibling recipes named <recipe>-<variant> — DRY siblings that share a base. Opt-in and additive on every subsystem's recipes; no variants key ⇒ output is byte-identical.

buttons.recipes.ts
buttons: {
  primary: {                     // the recipe — its own props ARE the base
    colors: "solid.primary", typography: "button.large", borders: "box.default",
    css: { gap: "8px" },
    variants: {                  // modifiers on THIS recipe
      sm:   { typography: "button.small", css: { gap: "6px" } },
      flat: { borders: null },   // null drops the inherited ref
    },
  },
}
emitted classes
.dt-components-buttons-primary       /* the bare recipe still emits */
.dt-components-buttons-primary-sm    /* base + sm delta */
.dt-components-buttons-primary-flat  /* base, minus the borders ref */

How a variant merges onto the recipe

  • A ref / scalar prop replaces — a ref is one atomic pointer; if the variant sets it, it wins, else it inherits.
  • css shallow-merges by property{ …base.css, …delta.css }, so sm nudges only gap.
  • states merge by state name — union of keys; a shared state's declarations shallow-merge (delta wins).
  • responsive[] concatenatesbase ++ delta, delta appended (higher source order).

Rules & edge cases

  • null removes, "none" keeps. A prop set to null drops an inherited ref (borders: null). Not "none" — that stays a real value (border: none, effects "none", the radius.none variant).
  • Flat axis, no nesting — matching token variants. Multi-axis modifiers are out of scope.
  • Collisions throw. A desugared <recipe>-<variant> that collides with an existing sibling recipe is a build error — never a silent last-wins.
  • Same pattern as properties. A responsive variant: / target: on a recipe references a sibling recipe — and because variants desugar into group members, target: "primary-sm" resolves against the same pool. See Variants & targets.
The variants map is a reserved recipe-leaf key alongside states / responsive. A pre-pass in the shared recipe normalizer expands it before anything downstream runs, so the Model and every adapter are unchanged.
Technical documentation for @theme-registry/refract · all output is compiled client-side by the real library (CSS adapter). The chrome is theme-aware; the render panes carry each preset's own world. · MIT licensed.