Compound variants
Styles that apply when more than one variant matches. Same authoring shape, different CSS for atomic vs config.
A variant is size or visual. A compound variant is a combination of two variants that only applies when both match.
Say size: 'sm' and visual: 'outline' together need tighter padding than either sets alone.
Every recipe API uses the same key: compoundVariants.
Writing a compound variant
A compound variante entry applies only for that exact pair.
const button = cva({
base: { rounded: 'md', fontWeight: 'semibold' },
variants: {
size: {
sm: { px: '3', py: '1.5' },
lg: { px: '5', py: '3' }
},
visual: {
solid: { bg: 'blue.500', color: 'white' },
outline: { borderWidth: '1px', color: 'blue.500' }
}
},
compoundVariants: [
{
size: 'sm',
visual: 'outline',
css: { px: '2', py: '1' }
}
]
})<button className={button({ size: 'sm', visual: 'outline' })}>Save</button>From the above, button({ size: 'sm', visual: 'outline' }) gets the tighter padding while button({ size: 'sm' }) alone gets the normal
size: 'sm' padding instead. Every key in a compound entry has to match, not just one.
A single key can however still match more than one value. Write it as an array, and it matches either one:
compoundVariants: [
{
size: ['sm', 'md'],
visual: 'outline',
css: { px: '2', py: '1' }
}
]size here matches sm or md, but visual still has to be outline.
Slot recipes
A slot recipe styles more than one part, so a compound variant needs to say which part each override belongs to. Instead
of one flat css object, pass a map keyed by slot name:
compoundVariants: [
{
size: 'sm',
visual: 'outline',
css: {
root: { px: '2' },
label: { fontWeight: 'semibold' }
}
}
]Here the sm + outline pair tightens root's padding and bolds label. Matching still works the same as above:
every key must match.
Atomic vs config recipe
You write a compound variant the same way for cva, sva, defineRecipe, and defineSlotRecipe. What Panda
generates for each is not the same.
For cva and sva, the compound's css merges into the recipe's other styles at call time and resolves to plain
atomic classes, the same px_2 and py_1 you'd get from any other style. There's no separate "compound" class:
@layer utilities {
.px_2 {
padding-inline: var(--spacing-2);
}
.py_1 {
padding-block: var(--spacing-1);
}
}For defineRecipe and defineSlotRecipe, Panda generates one named class for that combination ahead of time, in
recipes.compound_variants (or recipes.slots.compound_variants). Calling the recipe just appends that class name.
Nothing merges at runtime.
@layer recipes {
@layer compound_variants {
.button--compound__size_sm__visual_outline {
padding-inline: var(--spacing-2);
padding-block: var(--spacing-1);
}
}
}Eager emit vs smart compound variants
This only applies to defineRecipe and defineSlotRecipe. cva and sva resolve compounds inline on every call, so
there's nothing to opt into.
By default, the first time a config recipe is used anywhere in your code, Panda emits every compound variant it defines, not just the one that call needs. The right combo's class still gets applied at runtime. The rest just sit unused in your CSS.
optimize.smartCompoundVariants narrows that: only compound variants
matching a combination your code actually calls get emitted.
panda.config.ts
export default defineConfig({
optimize: { smartCompoundVariants: true }
})Turn it on, and a combo that only ever happens at runtime, never as a literal call Panda can see, won't get CSS.
Pre-generate those with staticCss instead.
Responsive variants
A config recipe normally accepts a responsive object as a variant value, size: { base: 'sm', md: 'lg' }. Add
compoundVariants to that recipe and Panda turns that off: the generated types drop ConditionalValue, and passing an
object at runtime throws instead of silently doing nothing:
[recipe:button:size] Conditions are not supported when using compound variants.
cva and sva never supported responsive variant props to begin with, compound variants or not, so there's nothing for
them to disable. The full story is on responsive variants.
Cascade layers
Inside a config recipe, the layers stack in this order, each one able to override the last:
recipes.base
recipes.variants
recipes.compound_variants
A compound variant beats the recipe's own base and variant styles for exactly that reason: recipes.compound_variants
comes after them in the stack. Slots use the same idea one level deeper: recipes.slots.base, recipes.slots.variants,
then recipes.slots.compound_variants.
@layer utilities comes after all of it, so css() calls and other utility overrides still win over any recipe,
compound or not.