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
createStyleContextfunction - Use the
withProviderandwithContextfunctions to create compound components
// components/ui/card.tsx
import { sva } from 'styled-system/css'
import { createStyleContext } 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 } = createStyleContext(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 createStyleContext function can also be used with slot recipes defined in the panda.config.ts file.
- Pass the config recipe to the
createStyleContextfunction - Use the
withProviderandwithContextfunctions to create compound components
// components/ui/card.tsx
import { card } from '../styled-system/recipes'
import { createStyleContext } from 'styled-system/jsx'
const { withProvider, withContext } = createStyleContext(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>
)
}
createStyleContext
This function is 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 createStyleContext 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>
Guides
Config Recipes
The rules of config recipes still applies when using createStyleContext. 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 } = createStyleContext(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 } = createStyleContext(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 } = createStyleContext(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 — 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']
})