Dynamic variants
Config recipes only emit variants Panda can see. Pre-generate the ones that come from props, state, or Storybook.
cva and sva emit every variant when you define them. This page is about config recipes (defineRecipe and
defineSlotRecipe).
The function still returns a class name at runtime. If that class was never written to the sheet, the styles do not apply.
<Button size="lg" /> // CSS is in the sheet
<Button size={size} /> // class name comes back, lg CSS does notThat is the usual "it works in Storybook, it is blank in the app" bug. Storybook had a literal. The app passed a prop.
What extract sees
button({ size: 'lg' }) // emits lg
button({ size: wide ? 'sm' : 'lg' }) // both branches, both literals
button({ size }) // defaultVariants only
<Button size="lg" /> // lg, if the tag matches
<Button size={size} /> // defaultVariants onlyPanda warns (recipe_variant_dynamic) when a variant value can't be determined statically and staticCss does
not already cover that key.
A renamed prop or a wrapper that is not the recipe name is a different miss. See Extraction rules and Tracking JSX.
Put a literal in source
A string at the call site, or a story that calls button({ size: 'lg' }), is enough. Extract sees it.
defaultVariants is what you get when the prop stays dynamic.
Use staticCss when you cannot write those literals in the app.
Pre-generate with staticCss
List the values the app will pass. The same RecipeRule works on the recipe or in staticCss.recipes.
button.recipe.ts
export const buttonRecipe = defineRecipe({
className: 'button',
staticCss: [{ size: ['sm', 'md', 'lg'] }]
})panda.config.ts
export default defineConfig({
staticCss: {
recipes: {
button: [{ size: ['sm', 'md'] }, { visual: ['*'] }]
}
}
})'*' on a key means every value of that variant. staticCss: ['*'] on the recipe, or staticCss.recipes: '*',
emits every variant. Fine for Storybook. Heavy for a production app.
The full list is Static CSS → Recipes.
One variant key per rule
{ size: ['sm'], visual: ['solid'] } in a single object is two usages, not the pair:
// emits size=sm and visual=solid, each with defaultVariants on the other key
{ size: ['sm'], visual: ['solid'] }
// not "sm + solid" as one combinationOther keys fall back to defaultVariants. That matters for
smart compound variants: one key per rule
does not invent a combo unless defaults fill the rest.