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>

PropsProvider

The PropsProvider lets you style a set of buttons, tags, or avatars as one group. Export it as ButtonGroup, TagGroup, or AvatarGroup. The group sets variant props once. Every withContext child picks them up.

export const Button = withContext('button')
export const ButtonGroup = 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.

Adding layout

When the group also lays out its children, split recipe props off and put style props on HStack. Use spaceX, not gap, when the overlap needs to be negative.

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 {...variantProps}>
      <HStack spaceX="-2" {...restProps} />
    </PropsProvider>
  )
}
<AvatarGroup size="lg" px="4">
  <Avatar />
  <Avatar />
</AvatarGroup>

size styles the avatars. px styles the stack.

Config recipe

Extract 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')
export const ButtonGroup = PropsProvider

cva already emits every variant, so the extra names do not matter there. The full tag rule is on Tracking JSX.

See also

Edit this page on GitHubView as markdown
Last updated on