Recipes Overview
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:
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.
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.
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():
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.
useUtilities() registers all standard CSS property mappings.Manual Utility Registration
Alternatively, you can register utilities manually if you only need specific ones:
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:
| Property | Type | Description |
|---|---|---|
name | string | The recipe name (used as the base class) |
base | object | Base declarations applied to all variants |
variants | object | Variant groups with their options |
defaultVariants | object | Default variant selections |
compoundVariants | array | Conditional 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.
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:
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:
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:
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"
Compound Variants
Use compoundVariants to define special styling for specific variant combinations:
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
- After base and variant classes are resolved, the runtime checks each compound variant
- For each compound variant, it compares the current variant selections against the
matchobject - If all match conditions are satisfied, the
cssdeclarations are added to the class string - 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
Utilities are individual CSS classes that apply a single style property, while recipes are runtime functions that combine multiple utility classes based on variant props.
| Feature | Utilities | Recipes |
|---|---|---|
| Output | Single CSS class | Multiple combined classes |
| Usage | Direct class application | Function call with props |
| Variants | No built-in variants | Full variant system |
| Runtime | No runtime needed | Requires @styleframe/runtime |
Recipes are ideal for component-level styling systems where you need variant selection, while utilities are great for one-off styling or building your own abstractions.
Very similar! Styleframe recipes are inspired by these excellent libraries and provide a similar developer experience:
- CVA (Class Variance Authority): Recipes function similarly to CVA's
cva()function, but are tightly integrated with Styleframe's token and utility system. - Panda CSS: Like Panda's recipes, Styleframe generates utility classes at build time with zero runtime CSS generation.
- Stitches: Similar variant API with
variants,defaultVariants, andcompoundVariants.
The key difference is that Styleframe recipes are part of a complete design system toolkit that includes variables, selectors, themes, and utilities - all working together with shared tokens. Not only that, but you also have complete control over the utility definitions that power your recipes.
Currently, recipes are standalone definitions. However, you can achieve composition by:
- Sharing variant objects: Extract common variants into reusable objects
- Using composables: Create factory functions that generate recipe configurations
- Combining at runtime: Merge multiple recipe outputs in your component
import { styleframe } from "styleframe";
import { useUtilities } from "@styleframe/theme";
const s = styleframe();
const { variable, ref, recipe } = s;
useUtilities(s);
// Define design tokens
const spacingSm = variable("spacing.sm", "0.5rem");
const spacingMd = variable("spacing.md", "1rem");
const spacingLg = variable("spacing.lg", "1.5rem");
// Shared variants
const sizeVariants = {
sm: { padding: ref(spacingSm) },
md: { padding: ref(spacingMd) },
lg: { padding: ref(spacingLg) },
};
// Reuse in multiple recipes
recipe({
name: "button",
variants: { size: sizeVariants, /* ... */ },
});
recipe({
name: "input",
variants: { size: sizeVariants, /* ... */ },
});
Themes
Styleframe themes provide powerful design system theming capabilities with type-safe variable overrides. Create consistent light/dark modes, brand variants, and user personalization with ease.
Runtime
Learn how to use Styleframe recipes in your components with the runtime package. Understand how variant resolution works and how to integrate recipes into your React, Vue, or other framework components.