Skip to content

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

JSX Usage

Tracking JSX

How Panda connects a JSX tag to a config recipe, and what breaks that connection.

Config recipes emit CSS for <Button size="lg" /> only if Panda can treat that tag as the recipe. The match is the component name, not the import. A local function named Button is enough, even if it never imported styled-system/recipes.

cva and sva do not need this. Defining them already emits every variant, so a missed JSX tag does not leave you without CSS.

Naming guide

A config recipe has three names. Mixing them up is the usual reason tracking fails.

theme: {
  recipes: {
    button: defineRecipe({
      className: 'btn', // .btn, .btn--size_sm
      jsx: ['Button'] // <Button>
    })
  }
}
import { button } from '../styled-system/recipes' // theme key
 
button({ size: 'sm' }) // btn btn--size_sm
 
<Button size="lg" /> // still Button, not Btn

Default tags

theme.recipes.button      →  Button
theme.slotRecipes.card    →  Card, Card.Root, CardRoot

Those names come from the theme key, not from className. The exported compound name still has to match. Card works. Panel does not, unless you set jsx.

const { withProvider, withContext } = createSlotRecipeContext(card)
 
// tracked
export const Card = {
  Root: withProvider('div', 'root'),
  Header: withContext('header', 'header')
}
 
// not tracked
export const Panel = {
  Root: withProvider('div', 'root'),
  Header: withContext('header', 'header')
}
<Card.Root size="lg">
  <Card.Header />
</Card.Root>

size is on the root. Header is a part.

jsx

Use jsx when the export name is not the default tag.

export const buttonRecipe = defineRecipe({
  className: 'button',
  jsx: ['Button', 'LinkButton', /Btn$/]
})

Strings are exact tags. A regex matches the full tag, including Button.Root.

⚠️

jsx replaces the default root name. It does not add to it. If you set jsx: ['LinkButton'] and omit Button, <Button> is no longer tracked.

On a slot recipe, jsx replaces the root name only. Slot compounds stay {ThemeKey}.{Slot} and {ThemeKey}{Slot}.

export const cardRecipe = defineSlotRecipe({
  className: 'card',
  slots: ['root', 'header'],
  jsx: ['Panel']
  // tracked:     Panel, Card.Root, Card.Header
  // not tracked: Card, Panel.Root, Panel.Header
})

If you export Panel = { Root, Header }, list the tags that carry variant props (Panel.Root), plus Panel if you render it. Or use a regex:

jsx: ['Panel', 'Panel.Root']
// or
jsx: [/^Panel/]

Keep Card / Card.Root in that list if those tags still appear in the app.

False matches

A local <Button> that is not this recipe can still be treated as one. The match is not import-gated.

A headless prop can also share a name with a CSS property (Select.Content position). You cannot exclude that tag in the current engine. If it causes a real problem, open an issue (opens in a new tab).

Renamed props (sizebuttonSize) and a wrapper named Random are a different miss. See Extraction rules.

See also

Edit this page on GitHubView as markdown
Last updated on