Skip to content

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

JSX Usage

Recipe context

styled() plus a provider. Build a compound component around a single-element recipe.

createRecipeContext is styled() plus a provider. A parent sets variant props once. Every withContext child picks them up. That is how you build a compound component around a single-element recipe.

You need jsxFramework in your config, or styled-system/jsx is not generated. This helper is a Client Component ("use client").

It works with cva, a config recipe, or an inline recipe definition.

import { cva } from '../styled-system/css'
import { createRecipeContext } from '../styled-system/jsx'
 
const buttonRecipe = cva({
  base: { display: 'inline-flex' },
  variants: {
    size: {
      sm: { px: '3' },
      lg: { px: '5' }
    }
  }
})
 
const { withContext, PropsProvider } = createRecipeContext(buttonRecipe)
 
export const Button = withContext('button')
<Button size="lg" type="submit" onClick={onSave}>
  Save
</Button>

size is the variant. type and onClick go to the <button>.

A config recipe is the same call with a different import:

import { button } from '../styled-system/recipes'
import { createRecipeContext } from '../styled-system/jsx'
 
const { withContext, PropsProvider } = createRecipeContext(button)

withContext

withContext(
  component, // 'button' or Link
  options?   // same as styled()
)

options is the same object as styled().

Wrapping an element

One recipe can back more than one component. Button is a button. LinkButton is an a. Same variants.

export const Button = withContext('button')
export const LinkButton = withContext('a')
<Button size="lg">Save</Button>
<LinkButton size="lg" href="/docs">
  Docs
</LinkButton>

For a config recipe, list both tags. See Config recipe.

Wrapping an external component

Pass the component you don't own.

import { ark } from '@ark-ui/react/factory'
import Link from 'next/link'
 
export const Button = withContext(ark.button, {
  defaultProps: { type: 'button' }
})
 
export const LinkButton = withContext(Link)

Default props

export const Button = withContext('button', {
  defaultProps: {
    type: 'button',
    size: 'sm'
  }
})

Incoming props win over defaults, so <Button size="lg" /> still works.

unstyled

Skip the recipe classes. Style props, css, and className still apply. Same idea as styled().

<Button unstyled css={{ bg: 'red.500' }}>
  Click me
</Button>

Sharing variant props

PropsProvider passes recipe props to every withContext child below it. It takes a value, so wrap it in your own group component, like ButtonGroup, TagGroup, or AvatarGroup. Split the variant props off for the provider and pass the rest to the element the group renders.

export const Button = withContext('button')
 
export function ButtonGroup(props) {
  const [variantProps, restProps] = buttonRecipe.splitVariantProps(props)
  return (
    <PropsProvider value={variantProps}>
      <div role="group" {...restProps} />
    </PropsProvider>
  )
}
<ButtonGroup size="lg" visual="outline">
  <Button>Save</Button>
  <Button visual="solid">Cancel</Button>
</ButtonGroup>

Save inherits size="lg" and visual="outline". Cancel keeps the size and overrides visual. Incoming props win over the group.

Avatar group

The group can render a layout component instead of a div.

import { HStack } from '../styled-system/jsx'
 
const { withContext, PropsProvider } = createRecipeContext(avatar)
 
export const Avatar = withContext('div')
 
export function AvatarGroup(props) {
  const [variantProps, restProps] = avatar.splitVariantProps(props)
  return (
    <PropsProvider value={variantProps}>
      <HStack spaceX="-2" {...restProps} />
    </PropsProvider>
  )
}
<AvatarGroup size="lg" px="4">
  <Avatar />
  <Avatar />
</AvatarGroup>

size styles the avatars. px styles the stack.

Config recipe

Panda sees size on ButtonGroup, not on Button. A renamed export like LinkButton is the same miss. List every tag that carries variant props, or the CSS is missing:

export const buttonRecipe = defineRecipe({
  className: 'button',
  jsx: ['Button', 'ButtonGroup', 'LinkButton']
})
 
export const LinkButton = withContext('a')

cva already emits every variant, so the extra names do not matter there. The full tag rule is on Tracking JSX. To skip listing tags on a config recipe, pregenerate its variants with staticCss.

See also

Edit this page on GitHubView as markdown
Last updated on