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. When size is sm and visual is
outline, you might want tighter padding than either variant would set alone.
Every recipe API uses the same key: compoundVariants.
Writing a compound variant
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>size and visual still apply. The compound variant adds the tighter padding only for that pair. Every listed key must
match. An array on a condition is OR, so size: ['sm', 'md'] matches either sm or md.
Slot recipes
On a slot recipe, css is per slot:
compoundVariants: [
{
size: 'sm',
visual: 'outline',
css: {
root: { px: '2' },
label: { fontWeight: 'semibold' }
}
}
]Atomic vs config recipe
The authoring is the same. The CSS is not. Same css: { px: '2', py: '1' } on sm + outline:
cva / sva atomize it. Runtime merges those objects into css(). There is no compound variant class.
@layer utilities {
.px_2 {
padding-inline: var(--spacing-2);
}
.py_1 {
padding-block: var(--spacing-1);
}
}defineRecipe / defineSlotRecipe emit a named class in recipes.compound_variants (or
recipes.slots.compound_variants). Runtime only appends that name.
@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
By default, that first use emits every compound variant for the recipe. Runtime still decides which class to apply, so unused combinations sit in the sheet until you opt into something tighter.
optimize.smartCompoundVariants emits only compound variants that
match a combination Panda actually extracted, plus defaults.
panda.config.ts
export default defineConfig({
optimize: { smartCompoundVariants: true }
})If a combo only exists at runtime, pair the flag with staticCss. Otherwise the
class name comes back and the CSS does not.
staticCss is one variant key per rule. Listing { size: ['sm'] } and { visual: ['outline'] } as two rules does
not emit the sm + outline combo unless defaults fill the other key.
Responsive variants
Adding compoundVariants disables responsive and conditional variant props on config recipes. Types drop
ConditionalValue. Runtime throws:
[recipe:button:size] Conditions are not supported when using compound variants.
cva and sva never had responsive variant props. The longer version is on
responsive variants.
Cascade layers
Config recipe layers, in order:
recipes.base
recipes.variants
recipes.compound_variants
Slots use recipes.slots.base, recipes.slots.variants, then recipes.slots.compound_variants. After that comes
@layer utilities.
Compound variants beat the recipe's own styles. css() and other utilities still win.