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.