Skip to content

Want to skip the docs? Check out pandamastery.com - the best way to learn Panda CSS

Advanced

Dynamic variants

Config recipes only emit variants Panda can see. Pre-generate the ones that come from props, state, or Storybook.

A config recipe (defineRecipe and defineSlotRecipe), only generates CSS for variant values Panda can see in your source, not ones that only exist at runtime (a prop, state, anything not literal).

Fix it one of two ways: put a literal call somewhere Panda scans, or list the value with staticCss, both covered below.

button({ size }) still returns a class name at runtime even when size is dynamic. It just might point at a class that was never written to the stylesheet:

<Button size="lg" /> // 'lg' is a literal, its CSS is in the sheet
<Button size={size} /> // size is a prop, class name comes back, but the lg CSS isn't there

That's the usual "it works in Storybook, but is blank in the app" bug: the story called button({ size: 'lg' }) with a literal. The app passed a prop instead.

What extract sees

Check your own code against this list before reaching for either fix:

button({ size: 'lg' }) // literal: emits size=lg
button({ size: wide ? 'sm' : 'lg' }) // both branches are literals: emits both
button({ size }) // size isn't a literal: only defaultVariants emit
 
<Button size="lg" /> // literal: emits size=lg, if the tag matches a recipe
<Button size={size} /> // not a literal: only defaultVariants emit

A renamed prop or a wrapper component with a different name is a different issue, not a dynamic-value problem: Panda never matched the recipe at all. See Extraction rules and Tracking JSX for those.

Put a literal in source

To fix it, put one literal call somewhere Panda scans. button({ size: 'lg' }) in a Storybook story is enough. It doesn't have to sit at the same call site as the dynamic prop.

Use staticCss when you cannot write those literals in the app.

Pre-generate with staticCss

Tell Panda which values to generate up front, instead of relying on a literal call existing somewhere. The same RecipeRule shape works whether you write it on the recipe itself 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 generates every value of that variant. staticCss: ['*'] on the recipe, or staticCss.recipes: '*', generates every variant it has. Fine for Storybook, where you want every state visible. Heavy for a production app that only ever uses a handful of them.

The full list is Static CSS → Recipes.

One variant key per rule

Each key in a staticCss rule generates independently. It's not one combined usage. { size: ['sm'], visual: ['solid'] } in a single rule object runs as two separate generations: one for size: 'sm' (with visual falling back to its default), and one for visual: 'solid' (with size falling back to its default).

Neither one is sm + solid together as a specific pair:

// two separate usages, not one combined usage
{ size: ['sm'], visual: ['solid'] }

staticCss can't express a two-key combination directly. Write a real literal call with both values instead (Put a literal in source above).

Alternatively, lean on compound variants: a rule matching one key still lets a compoundVariants entry emit, as long as the other key's value comes from defaultVariants.

See also

Edit this page on GitHubView as markdown
Last updated on