API Essentials

Recipes Overview

Styleframe recipes provide powerful component variants with type-safe configuration options. Create flexible, reusable UI components with consistent styling patterns and runtime variant selection.

Recipes in Styleframe are advanced component styling systems that combine utility declarations with configurable variants. They provide a powerful way to create flexible, reusable UI components with type-safe variant selection, default configurations, and compound variant support for complex styling scenarios.

Why use recipes?

Recipes help you:

  • Create consistent component variants: Define utility declarations and systematic variations for buttons, cards, inputs, and other UI components.
  • Enable flexible configuration: Provide multiple variant axes (size, color, state) that can be combined in any way your design system requires.
  • Maintain type safety: Get full TypeScript support for variant names and values, preventing invalid combinations at compile time.
  • Support complex scenarios: Use compound variants to handle specific combinations that need special styling treatment.

How Recipes Work

1. Define Your Recipe

First, you define a recipe in your Styleframe configuration:

styleframe.config.ts
import { styleframe } from "styleframe";
import { useUtilities } from "@styleframe/theme";

const s = styleframe();
const { variable, ref, recipe } = s;

// Register all utilities for recipe auto-generation
useUtilities(s);

// Define your design tokens
const spacingMd = variable("spacing.md", "1rem");
const borderWidthThin = variable("border-width.thin", "1px");
const colorPrimary = variable("color.primary", "#3b82f6");
const colorSecondary = variable("color.secondary", "#64748b");

// Define once in your config
recipe({
    name: "button",
    base: {
        padding: ref(spacingMd),
        borderWidth: "@border-width.thin",
        borderStyle: "solid",
    },
    variants: {
        color: {
            primary: { background: ref(colorPrimary) },
            secondary: { background: ref(colorSecondary) },
        },
    },
});

2. Generate Runtime Code

After you define a recipe, Styleframe auto-generates a TypeScript file with functions that you can call with variant props. This function returns a string of utility class names based on your configuration.

The @styleframe/runtime runtime package provides lightweight functions that power recipe variant selection. It's tree-shakeable, so you only import what you use.

recipes.ts (auto-generated)
import { createRecipe } from "@styleframe/runtime";
import type { RecipeRuntime } from "@styleframe/runtime";

const buttonRecipe = {
    base: {
        padding: "md",
        borderWidth: "thin",
        borderStyle: "[solid]",
    },
    variants: {
        color: {
            primary: {
                background: "primary",
            },
            secondary: {
                background: "secondary",
            },
        }
    }
} as const satisfies RecipeRuntime;

export const button = createRecipe("button", buttonRecipe);

3. Use in Components

The recipe function handles all the logic of combining base styles with your selected variants, so you just pass in the props you want.

src/components/Button.tsx
import { button } from "virtual:styleframe";

// Use in your components
button({ color: "primary" })
// Output: "button _padding:md _border-width:thin _border-style:[solid] _background:primary"

button({ color: "secondary" })
// Output: "button _padding:md _border-width:thin _border-style:[solid] _background:secondary"

Defining Recipes

You define a recipe using the recipe() function from your styleframe instance.

Before defining recipes, you need to register the utility factories that will power the auto-generation of utility classes from your recipe declarations. The easiest way to do this is with useUtilities():

styleframe.config.ts
import { styleframe } from "styleframe";
import { useUtilities } from "@styleframe/theme";

const s = styleframe();
const { recipe } = s;

// Register all utility factories at once
useUtilities(s);

// Now define your recipes - utilities auto-generate from declarations
recipe({
    name: "button",
    base: {
        display: "flex",
        padding: "1rem",
        borderRadius: "0.5rem",
    },
    variants: {
        size: {
            sm: { padding: "0.5rem" },
            lg: { padding: "1.5rem" },
        },
    },
});

export default s;

When you use a CSS property like padding or display in your recipe declarations, Styleframe automatically creates the corresponding utility classes (e.g., _padding:[1rem], _display:flex). The useUtilities() function registers all the utility factories needed for this auto-generation to work.

Why register utilities? Recipe declarations are converted to utility class names at runtime. For this to work, Styleframe needs to know how each CSS property maps to its utility class format. useUtilities() registers all standard CSS property mappings.

Manual Utility Registration

Alternatively, you can register utilities manually if you only need specific ones:

styleframe.config.ts
import { styleframe } from "styleframe";

const s = styleframe();
const { variable, ref, utility, recipe } = s;

// Define your tokens
const borderWidthThin = variable("border-width.thin", "1px");
const colorPrimary = variable("color.primary", "#3b82f6");
const colorSecondary = variable("color.secondary", "#64748b");
const colorWhite = variable("color.white", "#ffffff");
const spacingSm = variable("spacing.sm", "0.5rem");
const spacingMd = variable("spacing.md", "1rem");
const spacingLg = variable("spacing.lg", "1.5rem");

// Define utilities that the recipe will use
utility("background", ({ value }) => ({ backgroundColor: value }));
utility("color", ({ value }) => ({ color: value }));
utility("padding", ({ value }) => ({ padding: value }));
utility("border-width", ({ value }) => ({ borderWidth: value }));
utility("border-style", ({ value }) => ({ borderStyle: value }));

// Define the recipe
recipe({
    name: "button",
    base: {
        borderWidth: ref(borderWidthThin),
        borderStyle: "solid",
    },
    variants: {
        color: {
            primary: {
                background: ref(colorPrimary),
                color: ref(colorWhite),
            },
            secondary: {
                background: ref(colorSecondary),
                color: ref(colorWhite),
            },
        },
        size: {
            sm: {
                padding: ref(spacingSm),
            },
            md: {
                padding: ref(spacingMd),
            },
            lg: {
                padding: ref(spacingLg),
            },
        },
    },
    defaultVariants: {
        color: "primary",
        size: "md",
    },
});

export default s;

Recipe Options

The recipe() function accepts an options object with the following properties:

PropertyTypeDescription
namestringThe recipe name (used as the base class)
baseobjectBase declarations applied to all variants
variantsobjectVariant groups with their options
defaultVariantsobjectDefault variant selections
compoundVariantsarrayConditional variant combinations

Variants

Variants are the core feature of recipes, allowing you to define multiple styling options for each design dimension of your component. Each variant group (like color or size) contains named options that map to specific utility declarations.

styleframe.config.ts
import { styleframe } from "styleframe";
import { useUtilities } from "@styleframe/theme";

const s = styleframe();
const { variable, ref, recipe } = s;

useUtilities(s);

// Define design tokens
const colorPrimary = variable("color.primary", "#3b82f6");
const colorSecondary = variable("color.secondary", "#64748b");
const spacingSm = variable("spacing.sm", "0.5rem");
const spacingMd = variable("spacing.md", "1rem");
const spacingLg = variable("spacing.lg", "1.5rem");

recipe({
    name: "button",
    variants: {
        // "color" variant group with two options
        color: {
            primary: { background: ref(colorPrimary) },
            secondary: { background: ref(colorSecondary) },
        },
        // "size" variant group with three options
        size: {
            sm: { padding: ref(spacingSm) },
            md: { padding: ref(spacingMd) },
            lg: { padding: ref(spacingLg) },
        },
    },
});

When using the recipe, you select one option from each variant group:

src/components/Button.tsx
import { button } from "virtual:styleframe";

button({ color: "primary", size: "lg" })
// Output: "button _background:primary _padding:lg"

button({ color: "secondary", size: "sm" })
// Output: "button _background:secondary _padding:sm"

Default Variants

Use defaultVariants to specify which variant should be applied when no explicit variant is chosen:

styleframe.config.ts
import { styleframe } from "styleframe";
import { useUtilities } from "@styleframe/theme";

const s = styleframe();
const { recipe } = s;

useUtilities(s);

recipe({
    name: "button",
    base: { /* ... */ },
    variants: {
        color: {
            primary: { /* ... */ },
            secondary: { /* ... */ },
        },
        size: {
            sm: { /* ... */ },
            md: { /* ... */ },
            lg: { /* ... */ },
        },
    },
    defaultVariants: {
        color: "primary",
        size: "md",
    },
});

When you call button({}) without any props, it will use the default variants:

src/components/Button.tsx
import { button } from "virtual:styleframe";

button({})
// Output: "button _background:primary _color:white _padding:md"

button({ size: "lg" })
// Output: "button _background:primary _color:white _padding:lg"
// color still uses default "primary"
Pro tip: Choose default variants that work well in most common use cases. This reduces the amount of configuration needed when using your recipes.

Compound Variants

Use compoundVariants to define special styling for specific variant combinations:

styleframe.config.ts
import { styleframe } from "styleframe";
import { useUtilities } from "@styleframe/theme";

const s = styleframe();
const { variable, recipe } = s;

useUtilities(s);

// Define design tokens
const colorPrimaryDark = variable("color.primary-dark", "#2563eb");
const colorSecondaryDark = variable("color.secondary-dark", "#475569");

recipe({
    name: "button",
    base: { /* ... */ },
    variants: {
        color: {
            primary: { /* ... */ },
            secondary: { /* ... */ },
        },
        disabled: {
            false: {},
            true: {
                opacity: "@opacity.50",
                cursor: "not-allowed",
            },
        },
    },
    compoundVariants: [
        {
            // When color is primary AND disabled is false
            match: {
                color: "primary",
                disabled: false,
            },
            css: {
                hover: {
                    background: "@color.primary-dark",
                },
            },
        },
        {
            // When color is secondary AND disabled is false
            match: {
                color: "secondary",
                disabled: false,
            },
            css: {
                hover: {
                    background: "@color.secondary-dark",
                },
            },
        },
    ],
});

Compound variants are applied only when all conditions in match are satisfied.

How Compound Variants Work

  1. After base and variant classes are resolved, the runtime checks each compound variant
  2. For each compound variant, it compares the current variant selections against the match object
  3. If all match conditions are satisfied, the css declarations are added to the class string
  4. Multiple compound variants can match and all their styles will be applied

Use Cases for Compound Variants

Compound variants are ideal for:

  • Conditional hover/focus states: Only apply hover effects when a component isn't disabled
  • Theme-specific variations: Different colors based on size + theme combinations
  • State combinations: Special styling when multiple boolean variants are active together
  • Override cascading: Apply specific styles that should take precedence for certain combinations

Best Practices

  • Provide sensible defaults: Choose default variants that work well in most common use cases.
  • Use compound variants strategically: Only create compound variants when the combination needs special handling beyond simple composition.
  • Keep match conditions minimal: The more conditions in a match, the harder it is to maintain. Aim for 2-3 conditions maximum.
  • Document complex combinations: Add comments explaining why specific compound variants exist.

Frequently Asked Questions