Config Recipe
Shared recipes in your theme. defineRecipe for one element, defineSlotRecipe for parts. Named classes, JIT emit.
defineRecipe is the recipe that lives in your theme. You register it, run codegen, and import a function from
styled-system/recipes. You get named classes, not a bag of atomic classes.
Panda generates styles for config recipes just-in-time, meaning it emits a variant only when it can see that value in your source.
Defining the recipe
The authoring shape is the same as cva. The extra field that matters is className, which becomes the CSS stem.
button.recipe.ts
import { defineRecipe } from '@pandacss/dev'
export const buttonRecipe = defineRecipe({
className: 'button',
base: {
display: 'inline-flex',
alignItems: 'center',
rounded: 'md',
fontWeight: 'semibold'
},
variants: {
visual: {
solid: { bg: 'blue.500', color: 'white' },
outline: { borderWidth: '1px', borderColor: 'blue.500', color: 'blue.500' }
},
size: {
sm: { px: '3', py: '1.5', fontSize: 'sm' },
lg: { px: '5', py: '3', fontSize: 'md' }
}
},
defaultVariants: {
visual: 'solid',
size: 'sm'
}
})Adding it to the theme
Register it under a theme key. That key is the import name and the name extract looks for.
panda.config.ts
import { defineConfig } from '@pandacss/dev'
import { buttonRecipe } from './button.recipe'
export default defineConfig({
theme: {
extend: {
recipes: {
button: buttonRecipe
}
}
}
})After pnpm panda codegen:
import { button } from '../styled-system/recipes'
<button className={button({ visual: 'solid', size: 'lg' })}>Click me</button>Extending a preset recipe
If the recipe comes from a preset, override it under theme.extend.recipes. You can change className, base,
variants, and jsx. See The extend keyword.
export default defineConfig({
theme: {
extend: {
recipes: {
button: {
className: 'btn',
base: { rounded: 'full' },
jsx: ['Button', 'LinkButton']
}
}
}
}
})CSS Output
Panda emits a variant only when it can see a literal: button({ size: 'lg' }), a ternary whose both branches fold,
or <Button size="lg" /> on a tag it treats as this recipe. The first use also emits base plus
defaultVariants.
With the default separator (_), the classes look like this:
button
button--visual_solid
button--size_lg
@layer recipes.base {
.button {
display: inline-flex;
}
}
@layer recipes.variants {
.button--size_lg {
padding-inline: var(--spacing-5);
font-size: var(--font-sizes-md);
}
}hash, prefix, and separator change that string. They are baked into recipes/runtime.ts at codegen, so you
re-run codegen after flipping them.
Compounds are named classes in recipes.compound_variants, not leftover atoms. See
compound variants.
button({ size }) and <Button size={size} /> only emit defaultVariants. The function still returns a class
name at runtime. If that class was never written to the sheet, nothing paints. Pre-generate the missing values with
dynamic variants.
How the recipe is named
The import, the CSS class, and the JSX tag can all be different words. Mixing them up is the usual reason styles vanish.
Theme key
The key you register under theme.recipes is the import name and the name of the generated types.
export default defineConfig({
theme: {
extend: {
recipes: {
button: buttonRecipe
}
}
}
})import { button, type ButtonVariant } from '../styled-system/recipes'className
className on the recipe is only the CSS stem. It does not rename the import. Omit it and the stem falls back to
the theme key.
export const buttonRecipe = defineRecipe({
className: 'btn',
// ...
}).btn {
}
.btn--size_sm {
}JSX name
Panda connects a JSX tag to the recipe by the component name, not by the import. For theme key button, that
default name is Button.
<Button size="lg" />If you wrap that in another component with a different name, list both on jsx. jsx replaces the default
list, so keep Button if you still render it.
export function LinkButton(props) {
return <Button {...props} />
}export const buttonRecipe = defineRecipe({
className: 'btn',
jsx: ['Button', 'LinkButton']
})More on wrappers and missed tags: Tracking JSX.
Using raw()
On cva and sva, raw()
returns the resolved style object. Config recipes do not do that.
raw() is an identity helper: it returns the props you passed, so the extractor can see them.
button.raw({ size: 'sm' })
// { size: 'sm' }css(button.raw({ size: 'sm' }), { color: 'red.500' }) does not include the recipe styles. Merge the class string
with atomic CSS instead. Utilities sit above @layer recipes, so the extra styles win.
import { css, cx } from '../styled-system/css'
import { button } from '../styled-system/recipes'
cx(button({ size: 'sm' }), css({ color: 'red.500' }))Exported Types
Generated names follow the theme key, not className.
import { button, type ButtonVariant, type ButtonVariantProps } from '../styled-system/recipes'
type ButtonVariant = {
size?: 'sm' | 'lg'
visual?: 'solid' | 'outline'
}If the recipe has compounds, ButtonVariantProps is literals only. ConditionalValue goes away.
Breakpoint objects (size: { base: 'sm', md: 'lg' }) are responsive variants.
They only work when the recipe has no compoundVariants.
Helpers
button.variantKeys
// ['visual', 'size']
button.variantMap
// { visual: ['solid', 'outline'], size: ['sm', 'lg'] }
button.splitVariantProps({ size: 'sm', onClick() {} })
// [{ size: 'sm' }, { onClick() {} }]
button.getVariantProps({ size: 'lg' })
// { visual: 'solid', size: 'lg' }
button.raw({ size: 'sm' })
// { size: 'sm' }splitVariantProps is the one you want in a wrapper, so onClick never looks like a variant. variantMap is
handy for Storybook argTypes. merge(other) combines two recipes. getVariantProps fills in defaultVariants.
raw() is the identity helper above, not a style object.
Config Slot Recipe
Parts use defineSlotRecipe under theme.slotRecipes. Same contract as above: theme key, named classes, emit only
what extract sees. The function returns a slot map, not one class string.
The slot shape (slots, per-slot base / css) is the same as sva.
checkbox.recipe.ts
import { defineSlotRecipe } from '@pandacss/dev'
export const checkboxRecipe = defineSlotRecipe({
className: 'checkbox',
slots: ['root', 'control', 'label'],
base: {
root: { display: 'flex', alignItems: 'center', gap: '2' },
control: { borderWidth: '1px', borderRadius: 'sm' },
label: { marginStart: '2' }
},
variants: {
size: {
sm: { control: { boxSize: '8' }, label: { fontSize: 'sm' } },
md: { control: { boxSize: '10' }, label: { fontSize: 'md' } }
}
},
defaultVariants: { size: 'sm' }
})panda.config.ts
import { defineConfig } from '@pandacss/dev'
import { checkboxRecipe } from './checkbox.recipe'
export default defineConfig({
theme: {
extend: {
slotRecipes: {
checkbox: checkboxRecipe
}
}
}
})import { checkbox } from '../styled-system/recipes'
const classes = checkbox({ size: 'md' })
// classes.root, classes.control, classes.labelDo not wrap a slot recipe with styled(). Put the parts on JSX with
slot recipe context.
Config slot classes are named, not atomic:
checkbox__root
checkbox__control
checkbox__root--size_md
@layer recipes.slots.base {
.checkbox__root {
display: flex;
}
}
@layer recipes.slots.variants {
.checkbox__root--size_md {
/* … */
}
}Compounds emit per-slot classes in recipes.slots.compound_variants. See
compound variants.