Skip to content

Want to skip the docs? Check out pandamastery.com - the best way to learn Panda CSS

Variants

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.

See also

Edit this page on GitHubView as markdown
Last updated on