Skip to content

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

JSX Usage

Slot recipe context

sva plus a provider. Build a compound component around a slot recipe.

createSlotRecipeContext is for building styled compound components from a slot recipe. You wrap each part once. Callers set size on the root, and the other components pick it up via context.

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

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

import { sva } from '../styled-system/css'
import { createSlotRecipeContext } from '../styled-system/jsx'
 
const card = sva({
  slots: ['root', 'title', 'body'],
  base: {
    root: { rounded: 'lg', borderWidth: '1px', p: '4' },
    title: { fontWeight: 'semibold' },
    body: { color: 'gray.600', fontSize: 'sm' }
  },
  variants: {
    size: {
      sm: { root: { p: '3' } },
      lg: { root: { p: '6' } }
    }
  },
  defaultVariants: { size: 'sm' }
})
 
const { withProvider, withContext } = createSlotRecipeContext(card)
 
export const Card = {
  Root: withProvider('div', 'root'),
  Title: withContext('h2', 'title'),
  Body: withContext('div', 'body')
}
<Card.Root size="lg">
  <Card.Title>Hello</Card.Title>
  <Card.Body>A card with parts.</Card.Body>
</Card.Root>

size is set once on the root. Title and body read their classes from context.

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

import { card } from '../styled-system/recipes'
import { createSlotRecipeContext } from '../styled-system/jsx'
 
const { withProvider, withContext } = createSlotRecipeContext(card)

withProvider

The usual root. It splits variant props, provides the slot map, styles that slot, and sets data-slot to the slot name.

withProvider(
  component, // 'div' or Avatar.Root
  slot,      // 'root'
  options?   // same as styled()
)

options is the same object as styled().

Wrapping an element

export const Card = {
  Root: withProvider('div', 'root'),
  Title: withContext('h2', 'title'),
  Body: withContext('div', 'body')
}

Wrapping an external component

Pass the component you don't own. For a full headless tree, see Wrap headless libraries.

import { Avatar } from '@ark-ui/react/avatar'
 
export const AvatarRoot = withProvider(Avatar.Root, 'root')

withContext

Consumes context and applies one slot. It does not accept variant props. Those already live on the provider.

withContext(
  component, // 'h2' or Avatar.Image
  slot,      // 'title'
  options?   // same as withProvider
)

Each part also gets data-slot.

Wrapping an element

export const CardTitle = withContext('h2', 'title')
export const CardBody = withContext('div', 'body')

Wrapping an external component

import { Avatar } from '@ark-ui/react/avatar'
 
export const AvatarImage = withContext(Avatar.Image, 'image')
export const AvatarFallback = withContext(Avatar.Fallback, 'fallback')
⚠️

A withContext part throws if it renders outside its provider. Keep the tree under the matching withProvider or withRootProvider.

withRootProvider

Use this when the root does not render a DOM element. Some headless Root components are context-only. This helper provides the slot map and does not apply a slot class.

withRootProvider(
  component, // Dialog.Root
  options?   // defaultProps only
)
import { Dialog } from '@ark-ui/react/dialog'
 
const DialogRoot = withRootProvider(Dialog.Root, {
  defaultProps: { lazyMount: true }
})

The only option is defaultProps. There is no slot to style, so unstyled and factory options do not apply.

Default props

Use defaultProps for HTML or component props. Incoming props win.

const CardHeader = withContext('header', 'header', {
  defaultProps: { role: 'banner' }
})

Do not put variant keys here. withProvider splits variants off the incoming props only, so defaultProps: { size: 'sm' } does not pick the recipe. Set variant defaults on the recipe's defaultVariants.

unstyled

Skip this element's slot styles. Style props, css, and className still apply. The slot map in context does not change, so children keep their styles unless they pass unstyled too.

<AvatarRoot unstyled>
  <AvatarImage />
</AvatarRoot>

The root has no recipe class. The image still looks like the recipe.

<AvatarRoot>
  <AvatarImage unstyled css={{ bg: 'red.500' }} />
</AvatarRoot>

To strip every part, pass unstyled on each one you wrap.

withRootProvider does not style an element, so unstyled there does nothing. Put it on the parts.

Forwarding props

withProvider uses variant props to pick styles, then drops them so they do not land on the DOM. List a key in forwardProps when the wrapped component still needs the value, for example to mirror orientation as aria-orientation.

function TabsRoot({ orientation, ...rest }: TabsRootProps) {
  return <div aria-orientation={orientation} {...rest} />
}
 
export const Tabs = withProvider(TabsRoot, 'root', {
  forwardProps: ['orientation']
})

withContext has no variants to split. Its forwardProps is the factory allowlist: keep a prop that shares a name with a CSS property from becoming a style.

const TabIndicator = withContext(Indicator, 'indicator', {
  forwardProps: ['width']
})

Without that, width would be treated as a style prop.

Config recipe

Extract sees size on Card.Root, not on Title or Body. A renamed export like Panel is the same miss. List every tag that carries variant props, or the CSS is missing:

export const cardRecipe = defineSlotRecipe({
  className: 'card',
  slots: ['root', 'title', 'body'],
  jsx: ['Card', 'Card.Root', 'Panel', 'Panel.Root']
})

On a slot recipe, jsx replaces the root name only. Slot compounds stay {ThemeKey}.{Slot}. If you render Panel.Title, list that tag too. sva 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