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

styling
jsx style context

JSX Style Context

JSX Style Context provides an ergonomic way to style compound components with slot recipes.

It uses a context-based approach to distribute recipe styles across multiple child components, making it easier to style headless UI libraries like Ark UI, and Radix UI.

Atomic Slot Recipe

  • Create a slot recipe using the sva function
  • Pass the slot recipe to the createSlotRecipeContext function
  • Use the withProvider and withContext functions to create compound components
// components/ui/card.tsx
 
import { sva } from 'styled-system/css'
import { createSlotRecipeContext } from 'styled-system/jsx'
 
const card = sva({
  slots: ['root', 'label'],
  base: {
    root: {},
    label: {}
  },
  variants: {
    size: {
      sm: { root: {} },
      md: { root: {} }
    }
  },
  defaultVariants: {
    size: 'sm'
  }
})
 
const { withProvider, withContext } = createSlotRecipeContext(card)
 
const Root = withProvider('div', 'root')
const Label = withContext('label', 'label')
 
export const Card = {
  Root,
  Label
}

Then you can use the Root and Label components to create a card.

// app/page.tsx
 
import { Card } from './components/ui/card'
 
export default function App() {
  return (
    <Card.Root>
      <Card.Label>Hello</Card.Label>
    </Card.Root>
  )
}

Config Slot Recipe

The createSlotRecipeContext function can also be used with slot recipes defined in the panda.config.ts file.

  • Pass the config recipe to the createSlotRecipeContext function
  • Use the withProvider and withContext functions to create compound components
// components/ui/card.tsx
 
import { card } from '../styled-system/recipes'
import { createSlotRecipeContext } from 'styled-system/jsx'
 
const { withProvider, withContext } = createSlotRecipeContext(card)
 
const Root = withProvider('div', 'root')
const Label = withContext('label', 'label')
 
export const Card = {
  Root,
  Label
}

Then you can use the Root and Label components to create a card.

// app/page.tsx
 
import { Card } from './components/ui/card'
 
export default function App() {
  return (
    <Card.Root>
      <Card.Label>Hello</Card.Label>
    </Card.Root>
  )
}

createSlotRecipeContext

Use this for a slot recipe (sva or defineSlotRecipe) with multiple parts. It's a factory function that returns three functions: withRootProvider, withProvider, and withContext.

withRootProvider

Creates the root component that provides the style context. Use this when the root component does not render an underlying DOM element.

import { Dialog } from '@ark-ui/react'
 
//...
 
const DialogRoot = withRootProvider(Dialog.Root)

withProvider

Creates a component that both provides context and applies the root slot styles. Use this when the root component renders an underlying DOM element.

💡

Note: It requires the root slot parameter to be passed.

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

withContext

Creates a component that consumes the style context and applies slot styles. It does not accept variant props directly, but gets them from context.

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

unstyled prop

Every component created with createSlotRecipeContext supports the unstyled prop to disable styling. It is useful when you want to opt-out of the recipe styles.

  • When applied the root component, will disable all styles
  • When applied to a child component, will disable the styles for that specific slot
// Removes all styles
<AvatarRoot unstyled>
  <AvatarImage />
  <AvatarFallback />
</AvatarRoot>
 
// Removes only the styles for the image slot
<AvatarRoot>
  <AvatarImage unstyled css={{ bg: 'red' }} />
  <AvatarFallback />
</AvatarRoot>

createRecipeContext

Use this for a single-element config recipe (cva or defineRecipe) that has no slots. It returns a single withContext function, and withContext takes just the element or component, with no slot name.

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

The component accepts the recipe's variant props and forwards them to drive the styling.

Guides

Config Recipes

The rules of config recipes still applies when using createSlotRecipeContext. Ensure the name of the final component matches the name of the recipe.

💡

If you want to use a custom name, you can configure the recipe's jsx property in the panda.config.ts file.

// recipe name is "card"
import { card } from '../styled-system/recipes'
 
const { withRootProvider, withContext } = createSlotRecipeContext(card)
 
const Root = withRootProvider('div')
const Header = withContext('header', 'header')
const Body = withContext('body', 'body')
 
// The final component name must be "Card"
export const Card = {
  Root,
  Header,
  Body
}

Default Props

Use defaultProps option to provide default props to the component.

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

Forwarding props

withProvider uses variant props to style the slots, but doesn't pass them to your component. When you need a variant value inside the component, list it in forwardProps. It still styles the slot, and now your component receives it too.

const tabs = sva({
  slots: ['root', 'tab'],
  base: { root: { display: 'flex' } },
  variants: {
    orientation: {
      horizontal: { root: { flexDirection: 'row' } },
      vertical: { root: { flexDirection: 'column' } }
    }
  }
})
 
const { withProvider } = createSlotRecipeContext(tabs)
 
function TabsRoot({ orientation, ...rest }: TabsRootProps) {
  // `orientation` is available here, so we can mirror it as an aria attribute
  return <div aria-orientation={orientation} {...rest} />
}
 
export const Tabs = withProvider(TabsRoot, 'root', {
  forwardProps: ['orientation']
})

forwardProps works on withContext too. Since context consumers don't take variant props, it's mainly useful for forwarding props that share a name with a CSS property, since otherwise they'd be turned into styles instead of reaching your component.

// `width` would normally become a style. Forward it to the component instead
const TabIndicator = withContext(Indicator, 'indicator', {
  forwardProps: ['width']
})