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
svafunction - Pass the slot recipe to the
createSlotRecipeContextfunction - Use the
withProviderandwithContextfunctions 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
createSlotRecipeContextfunction - Use the
withProviderandwithContextfunctions 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']
})