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 BtnDefault 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.
Skip tracking with staticCss
If you'd rather not keep jsx in sync with your component names, pregenerate the recipe's variants with staticCss.
Panda then emits every variant whatever the tag is called, so a renamed export or a wrapper never leaves you without
CSS. That's how cva and sva already work.
export const cardRecipe = defineSlotRecipe({
className: 'card',
slots: ['root', 'header'],
variants: {
size: {
sm: { root: { padding: '2' } },
lg: { root: { padding: '6' } }
}
},
staticCss: ['*'] // every variant, whatever the tag is called
})Or list the recipes in your config:
panda.config.ts
export default defineConfig({
staticCss: {
recipes: {
button: ['*'],
card: ['*']
}
}
})The trade-off is size: you ship every variant, used or not. That's cheap for design-system recipes with a handful of
variants. Avoid recipes: '*' across a large app. See Static CSS.
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 (size → buttonSize) and a wrapper named Random are a different miss. See
Extraction rules.