Skip to content

createThemedRecipes

createThemedRecipes returns createStyleRecipe and createSlotStyleRecipe for a theme type. Each takes a function that returns the config of the recipe for a theme, and returns a themed recipe, which takes the theme first, then the selection.

import { createThemedRecipes } from "@lynstack/native-recipe";
interface Theme {
readonly colors: { readonly primary: string; readonly surface: string };
readonly radius: number;
}
const { createStyleRecipe } = createThemedRecipes<Theme>();
const button = createStyleRecipe((theme) => ({
base: { borderRadius: theme.radius },
variants: {
tone: {
primary: { backgroundColor: theme.colors.primary },
surface: { backgroundColor: theme.colors.surface },
},
},
defaultVariants: { tone: "primary" },
}));
const light: Theme = {
colors: { primary: "#2563eb", surface: "#ffffff" },
radius: 8,
};
const dark: Theme = {
colors: { primary: "#60a5fa", surface: "#111827" },
radius: 8,
};
button(light, { tone: "surface" });
// => { borderRadius: 8, backgroundColor: "#ffffff" }
button(dark, { tone: "surface" });
// => { borderRadius: 8, backgroundColor: "#111827" }

Call createThemedRecipes once, in a module of your own, and import its recipe creators wherever you create recipes (see Themes and design tokens). It creates nothing at runtime but the two functions; the type parameter is what it adds.

The function receives a theme and returns the config of a style recipe or a slot style recipe, with the same properties and the same checks. Its variants, options, and slots are inferred from what it returns, so a themed recipe has the same types as a recipe written without a theme, with the values of tokens typed as the theme types them.

The config of every theme must declare the same variants, options, and slots; only the styles may depend on the theme. Branch on a token’s value inside a style, never around a variant.

A themed recipe takes the theme, then the selection, which is optional when every variant is:

button(light); // => { borderRadius: 8, backgroundColor: "#2563eb" }
button(light) === button(light, { tone: "primary" }); // => true

It returns the same frozen styles for the same theme and variants. The first call with a theme object builds the config for that theme and compiles a recipe from it; later calls with the same object reuse it, so each theme has its own cache, kept when another theme is used:

const surface = button(light, { tone: "surface" });
button(dark, { tone: "surface" });
button(light, { tone: "surface" }) === surface; // => true

A theme object must keep its identity for its cache to last: a new object, even with the same tokens, compiles a new recipe (see Keep each theme stable). A theme that is no longer referenced is released with its recipe.

withTheme returns the recipe of one theme: a plain recipe, the same one for the same theme object, with the names of its variants in variantKeys.

const lightButton = button.withTheme(light);
lightButton({ tone: "surface" }) === button(light, { tone: "surface" }); // => true
lightButton.variantKeys; // => ["tone"]

Use it to read variantKeys, or to pass a recipe to code that takes a plain recipe, such as a child component that does not know about themes.

createSlotStyleRecipe works the same way for slot recipes:

const { createSlotStyleRecipe } = createThemedRecipes<Theme>();
const card = createSlotStyleRecipe((theme) => ({
slots: ["root", "title"],
base: { root: { borderRadius: theme.radius }, title: { fontSize: 16 } },
variants: {
raised: { true: { root: { backgroundColor: theme.colors.surface } } },
},
}));
card(dark, { raised: true });
// => { root: { borderRadius: 8, backgroundColor: "#111827" }, title: { fontSize: 16 } }

VariantsOf returns the variants of a themed recipe, as of any recipe:

import type { VariantsOf } from "@lynstack/native-recipe";
type ButtonVariants = VariantsOf<typeof button>;
// => { readonly tone?: "primary" | "surface" | undefined }

ThemedRecipe is the type of a themed recipe, and ThemedRecipeCreators the type of what createThemedRecipes returns.