20 KiB
name, description
| name | description |
|---|---|
| panda-recipe-patterns | Create and organize recipes (regular + slot recipes), compound variants, and understand when to use recipes vs patterns vs inline CSS |
Panda CSS Recipe Patterns
When to Use This Skill
Use this skill when:
- Creating reusable component styles with variants
- Building multi-part component styling (checkboxes, tooltips, menus)
- Defining compound variants (multiple conditions)
- Organizing component style libraries
- Deciding between recipes, patterns, and inline CSS
For implementing these recipes in React components, use the panda-component-impl skill.
When to Use What
Use Recipes When:
- Component has multiple style variants (e.g., button: primary, secondary, outline)
- Component has size variants (small, medium, large)
- Styles are reused across multiple instances
- Need compound variants (combining multiple variant conditions)
- Want to auto-apply styles to JSX components
- Building a design system with consistent component APIs
Use Patterns When:
- Need computed/transformed styles (e.g., icon sizing that sets width=height)
- Creating reusable layout primitives (stack, grid, container)
- Want a props-based API for common styling tasks
- Need to enforce constraints (e.g., size must be a valid token)
Use Inline CSS When:
- One-off styles specific to a single usage
- Dynamic values from props or state
- Component-specific overrides of recipe defaults
- Rapid prototyping before extracting to recipe
Example Decision Tree:
Button component with variants? → Recipe
Icon with size prop? → Pattern
Unique spacing on one div? → Inline CSS
Reusable card layout? → Recipe
Regular Recipes (Single-Part Components)
Basic Recipe Structure
Create: src/recipes/button.ts
import { defineRecipe } from '@pandacss/dev';
const buttonBase = {
position: 'relative',
appearance: 'none',
minWidth: '0',
transitionDuration: 'fast',
transitionProperty: 'background, border-color, color, box-shadow',
transitionTimingFunction: 'default',
userSelect: 'none',
verticalAlign: 'middle',
display: 'flex',
alignItems: 'center',
gap: '4',
fontFamily: 'body',
fontSize: '16',
fontWeight: 'medium',
lineHeight: 'default',
borderWidth: '1',
borderStyle: 'solid',
borderColor: 'transparent',
borderRadius: '4',
outlineWidth: '2',
outlineStyle: 'solid',
outlineColor: 'transparent',
outlineOffset: '1',
textDecoration: 'none',
whiteSpace: 'nowrap',
cursor: 'pointer',
_disabled: {
opacity: 0.4,
cursor: 'not-allowed',
},
_focusVisible: {
outlineColor: { base: 'slate.80', _dark: 'slate.5' },
},
'& svg': {
fill: 'current',
},
};
const buttonVariants = {
variant: {
primary: {
bg: { base: 'slate.90', _dark: 'slate.5' },
color: { base: 'slate.0', _dark: 'slate.90' },
_hover: {
bg: { base: 'slate.70', _dark: 'slate.10' },
},
_active: {
bg: { base: 'slate.100', _dark: 'slate.20' },
},
_disabled: {
_hover: {
bg: { base: 'slate.90', _dark: 'slate.5' },
},
},
_selected: {
bg: { base: 'slate.5', _dark: 'slate.90' },
color: { base: 'slate.90', _dark: 'slate.0' },
},
},
standard: {
bg: { base: 'slate.5', _dark: 'slate.70' },
color: { base: 'slate.90', _dark: 'slate.0' },
_hover: {
bg: { base: 'slate.10', _dark: 'slate.60' },
},
_active: {
bg: { base: 'slate.20', _dark: 'slate.80' },
},
_disabled: {
_hover: {
bg: { base: 'slate.5', _dark: 'slate.70' },
},
},
_selected: {
bg: { base: 'slate.90', _dark: 'slate.5' },
color: { base: 'slate.0', _dark: 'slate.90' },
},
},
hollow: {
bg: 'transparent',
borderColor: { base: 'slate.30', _dark: 'slate.60' },
color: { base: 'slate.90', _dark: 'slate.0' },
_hover: {
bg: { base: 'slate.10', _dark: 'slate.60' },
borderColor: { base: 'slate.10', _dark: 'slate.60' },
},
_active: {
bg: { base: 'slate.20', _dark: 'slate.80' },
borderColor: { base: 'slate.20', _dark: 'slate.80' },
},
_disabled: {
_hover: {
bg: 'transparent',
},
},
_selected: {
bg: { base: 'slate.90', _dark: 'slate.5' },
color: { base: 'slate.0', _dark: 'slate.90' },
borderColor: 'transparent',
},
},
ghost: {
bg: 'transparent',
color: { base: 'slate.90', _dark: 'slate.0' },
_hover: {
bg: { base: 'slate.10', _dark: 'slate.60' },
},
_active: {
bg: { base: 'slate.20', _dark: 'slate.70' },
},
_disabled: {
_hover: {
bg: 'transparent',
},
},
_selected: {
bg: { base: 'slate.90', _dark: 'slate.5' },
color: { base: 'slate.0', _dark: 'slate.90' },
},
},
cta: {
bg: { base: 'gold.20', _dark: 'gold.30' },
color: 'slate.90',
_hover: {
bg: { base: 'gold.10', _dark: 'gold.20' },
},
_active: {
bg: { base: 'gold.30', _dark: 'gold.40' },
},
_disabled: {
_hover: {
bg: { base: 'gold.20', _dark: 'gold.30' },
},
},
},
danger: {
bg: 'red.50',
color: 'slate.0',
_hover: {
bg: 'red.40',
},
_active: {
bg: 'red.60',
},
_disabled: {
_hover: {
bg: 'red.50',
},
},
},
},
};
export const buttonRecipe = defineRecipe({
className: 'button',
jsx: ['Button'],
base: buttonBase,
variants: {
...buttonVariants,
size: {
medium: {
fontSize: '16',
py: '3',
px: '10',
},
large: {
fontSize: '16',
py: '7',
px: '12',
},
small: {
fontSize: '14',
py: '0',
px: '8',
'& svg': {
mt: '-1',
mb: '-1',
},
},
},
},
defaultVariants: {
variant: 'standard',
size: 'medium',
},
});
export const iconButtonRecipe = defineRecipe({
className: 'icon-button',
jsx: ['IconButton'],
base: buttonBase,
variants: {
...buttonVariants,
size: {
medium: {
fontSize: '16',
p: '3',
},
large: {
fontSize: '16',
p: '7',
},
small: {
fontSize: '14',
p: '0',
'& svg': {
mt: '-1',
mb: '-1',
},
},
},
},
defaultVariants: {
variant: 'standard',
size: 'medium',
},
});
Extract Base Styles for DRY Code
Pattern: Share base styles between related recipes:
// Shared base for button and iconButton
const buttonBase = {
appearance: 'none',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
transitionDuration: 'fast',
_disabled: { opacity: 0.4, cursor: 'not-allowed' },
_focusVisible: { outlineWidth: '2', outlineColor: 'blue.50' }
}
// Shared variant styles
const buttonVariants = {
variant: {
primary: { /* ... */ },
secondary: { /* ... */ }
}
}
// Button recipe
export const buttonRecipe = defineRecipe({
className: 'button',
jsx: ['Button'],
base: buttonBase,
variants: {
...buttonVariants,
size: { /* button sizes */ }
},
defaultVariants: { variant: 'primary', size: 'medium' }
})
// IconButton recipe (reuses base and variants)
export const iconButtonRecipe = defineRecipe({
className: 'iconButton',
jsx: ['IconButton'],
base: buttonBase,
variants: {
...buttonVariants,
size: {
small: { width: '32', height: '32' },
medium: { width: '40', height: '40' },
large: { width: '48', height: '48' }
}
},
defaultVariants: { variant: 'primary', size: 'medium' }
})
Why: Keeps related components visually consistent, reduces duplication.
Dynamic Variants from Tokens
Pattern: Generate variants from design tokens:
import { tokens } from '../styles/tokens'
// Get fontSize tokens
const fontSizeTokens = tokens.fontSizes
type FontSizeKey = keyof typeof fontSizeTokens
// Generate fontSize variants dynamically
const fontSizes = (Object.keys(fontSizeTokens) as FontSizeKey[]).reduce(
(accumulator, currentKey) => {
accumulator[currentKey] = { fontSize: currentKey }
return accumulator
},
{} as Record<FontSizeKey, Record<'fontSize', string>>
)
export const textRecipe = defineRecipe({
className: 'text',
jsx: ['Text'],
variants: {
size: fontSizes // All token sizes available as variants
}
})
Why: Automatically sync recipe variants with token changes.
Slot Recipes (Multi-Part Components)
Use slot recipes for components with multiple styled parts (checkbox + label, tooltip + arrow, menu + items).
Basic Slot Recipe Structure
Create: src/recipes/checkbox.ts
import { defineSlotRecipe } from '@pandacss/dev'
export const checkBoxRecipe = defineSlotRecipe({
className: 'checkbox',
description: 'Checkbox component with label',
jsx: ['CheckBox'],
// Define named slots for component parts
slots: ['container', 'input', 'indicator', 'label'],
// Base styles for each slot
base: {
container: {
display: 'inline-flex',
alignItems: 'center',
gap: '8',
cursor: 'pointer',
position: 'relative'
},
input: {
// Visually hidden but accessible
position: 'absolute',
opacity: 0,
width: '1px',
height: '1px',
// Show different indicator icons based on state
_checked: {
"& ~ [data-part='indicator'][data-state='unchecked']": {
display: 'none'
},
"& ~ [data-part='indicator'][data-state='checked']": {
display: 'inline-flex'
}
},
_indeterminate: {
"& ~ [data-part='indicator'][data-state='indeterminate']": {
display: 'inline-flex'
}
},
_disabled: {
"& ~ [data-part='indicator']": {
opacity: 0.4,
cursor: 'not-allowed'
}
}
},
indicator: {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
width: '20',
height: '20',
borderWidth: '1',
borderRadius: '4',
borderColor: { base: 'slate.40', _dark: 'slate.60' },
bg: { base: 'white', _dark: 'slate.90' },
color: { base: 'blue.50', _dark: 'blue.40' }
},
label: {
fontSize: 'md',
color: { base: 'slate.90', _dark: 'slate.10' },
userSelect: 'none'
}
},
// Variants apply to specific slots
variants: {
size: {
small: {
indicator: { width: '16', height: '16' },
label: { fontSize: 'sm' }
},
medium: {
indicator: { width: '20', height: '20' },
label: { fontSize: 'md' }
},
large: {
indicator: { width: '24', height: '24' },
label: { fontSize: 'lg' }
}
}
},
defaultVariants: {
size: 'medium'
}
})
Complex State Handling
Pattern: Use sibling selectors for state-based styling:
base: {
input: {
// When checked, show checked icon, hide unchecked icon
_checked: {
"& ~ [data-part='indicator']": {
bg: { base: 'blue.50', _dark: 'blue.40' },
borderColor: { base: 'blue.50', _dark: 'blue.40' },
color: 'white'
}
},
// When focused, add focus ring to indicator
_focusVisible: {
"& ~ [data-part='indicator']": {
outlineWidth: '2',
outlineOffset: '1',
outlineColor: { base: 'blue.50', _dark: 'blue.40' }
}
},
// Error state
_invalid: {
"& ~ [data-part='indicator']": {
borderColor: { base: 'red.50', _dark: 'red.40' }
},
"& ~ [data-part='label']": {
color: { base: 'red.50', _dark: 'red.40' }
}
}
}
}
Advanced Slot Recipe: Tooltip
Create: src/recipes/tooltip.ts
import { defineSlotRecipe } from '@pandacss/dev'
export const tooltipRecipe = defineSlotRecipe({
className: 'tooltip',
jsx: ['Tooltip'],
slots: ['trigger', 'content', 'arrow'],
base: {
trigger: {
cursor: 'pointer'
},
content: {
position: 'absolute',
zIndex: 50,
px: '12',
py: '8',
fontSize: 'sm',
borderRadius: '6',
bg: { base: 'slate.90', _dark: 'slate.10' },
color: { base: 'white', _dark: 'slate.90' },
boxShadow: 'lg',
maxWidth: '320',
// Fade in/out animation
opacity: 0,
transitionProperty: 'opacity',
transitionDuration: 'fast',
_open: {
opacity: 1
}
},
arrow: {
position: 'absolute',
width: '8',
height: '8',
bg: { base: 'slate.90', _dark: 'slate.10' },
transform: 'rotate(45deg)'
}
},
variants: {
position: {
top: {
content: { bottom: 'full', left: '50%', transform: 'translateX(-50%)' },
arrow: { bottom: '-4', left: '50%', transform: 'translateX(-50%) rotate(45deg)' }
},
'top-start': {
content: { bottom: 'full', left: '0' },
arrow: { bottom: '-4', left: '12' }
},
'top-end': {
content: { bottom: 'full', right: '0' },
arrow: { bottom: '-4', right: '12' }
},
bottom: {
content: { top: 'full', left: '50%', transform: 'translateX(-50%)' },
arrow: { top: '-4', left: '50%', transform: 'translateX(-50%) rotate(45deg)' }
},
left: {
content: { right: 'full', top: '50%', transform: 'translateY(-50%)' },
arrow: { right: '-4', top: '50%', transform: 'translateY(-50%) rotate(45deg)' }
},
right: {
content: { left: 'full', top: '50%', transform: 'translateY(-50%)' },
arrow: { left: '-4', top: '50%', transform: 'translateY(-50%) rotate(45deg)' }
}
},
// Option to hide arrow
caret: {
true: {},
false: {
arrow: { display: 'none' }
}
}
},
// Compound variants: combine multiple variant conditions
compoundVariants: [
{
position: ['top', 'top-start', 'top-end'],
caret: true,
css: {
content: { mb: '12' } // Add margin when arrow is shown on top
}
},
{
position: ['bottom', 'bottom-start', 'bottom-end'],
caret: true,
css: {
content: { mt: '12' }
}
}
],
defaultVariants: {
position: 'top',
caret: true
}
})
Compound Variants
Use compound variants when combining multiple variant values requires unique styling.
Pattern: Conditional styling based on variant combinations:
export const buttonRecipe = defineRecipe({
variants: {
variant: {
primary: { /* ... */ },
outline: { /* ... */ }
},
size: {
small: { /* ... */ },
large: { /* ... */ }
},
loading: {
true: { cursor: 'wait' }
}
},
// Special styles when specific variants combine
compoundVariants: [
{
variant: 'primary',
loading: true,
css: {
bg: { base: 'blue.40', _dark: 'blue.30' }, // Lighter when loading
_hover: { bg: { base: 'blue.40', _dark: 'blue.30' } } // Disable hover
}
},
{
variant: 'outline',
size: 'small',
css: {
borderWidth: '1', // Thinner border for small outline
fontWeight: 'medium'
}
}
]
})
Recipe Organization
File Structure
src/
recipes/
index.ts # Export all recipes
button.ts # Regular recipe
input.ts # Regular recipe
checkbox.ts # Slot recipe
radio.ts # Slot recipe
tooltip.ts # Slot recipe
menu.ts # Slot recipe
Export Pattern
src/recipes/index.ts:
// Regular recipes
export { buttonRecipe } from './button'
export { iconButtonRecipe } from './button'
export { inputRecipe } from './input'
export { textRecipe } from './text'
// Slot recipes
export { checkBoxRecipe } from './checkbox'
export { radioRecipe } from './radio'
export { tooltipRecipe } from './tooltip'
export { menuRecipe } from './menu'
Register in Config
panda.config.ts:
import { defineConfig } from '@pandacss/dev'
import * as allRecipes from './src/recipes'
// Separate regular and slot recipes
const {
checkBoxRecipe,
radioRecipe,
tooltipRecipe,
menuRecipe,
...regularRecipes
} = allRecipes
// Transform keys: remove 'Recipe' suffix
const recipes = Object.fromEntries(
Object.entries(regularRecipes).map(([key, value]) => [
key.replace(/Recipe$/, ''), // buttonRecipe → button
value
])
)
const slotRecipes = {
checkbox: checkBoxRecipe,
radio: radioRecipe,
tooltip: tooltipRecipe,
menu: menuRecipe
}
export default defineConfig({
theme: {
extend: {
recipes,
slotRecipes
}
}
})
Why: Clean separation, automatic recipe registration.
Responsive Recipes
Pattern: Use object syntax for responsive variants:
export const cardRecipe = defineRecipe({
base: {
p: { base: '16', md: '20', lg: '24' }, // Responsive padding
borderRadius: { base: '6', md: '8' },
fontSize: { base: 'sm', md: 'md' }
}
})
Pattern: Container queries for component-level responsive:
export const menuRecipe = defineSlotRecipe({
base: {
container: {
// Change layout based on container size (not viewport)
width: { base: 'full', '@container(min-width: 768px)': '260' },
position: { base: 'fixed', '@container(min-width: 768px)': 'relative' }
}
}
})
Best Practices Checklist
Create TodoWrite items when creating recipes:
- Extract shared base styles for related components
- Use semantic tokens (not raw colors) in recipes
- Define sensible defaultVariants
- Include common state styles (_hover, _focus, _disabled, _active)
- Add _focusVisible for accessibility
- Use slot recipes for multi-part components
- Document recipe purpose in description field
- Use compound variants for complex combinations
- Test all variant combinations
- Validate recipes work in light AND dark themes
Common Pitfalls
Avoid: Hard-coded Values
// BAD: Hard-coded hex colors, px values
variants: {
primary: {
bg: '#3B82F6',
padding: '12px'
}
}
// GOOD: Use design tokens
variants: {
primary: {
bg: { base: 'blue.50', _dark: 'blue.40' },
padding: '12'
}
}
Avoid: Missing Default Variants
// BAD: No defaults, components render unstyled
variants: {
size: { small: {...}, large: {...} }
}
// GOOD: Always provide defaults
variants: {
size: { small: {...}, medium: {...}, large: {...} }
},
defaultVariants: {
size: 'medium'
}
Avoid: Over-nesting in Slot Recipes
// BAD: Deep nesting, hard to maintain
base: {
container: {
'& > div > span > button': { /* ... */ }
}
}
// GOOD: Use slots for structure
slots: ['container', 'wrapper', 'label', 'button'],
base: {
container: { /* ... */ },
button: { /* ... */ }
}
Avoid: Duplicate Logic Across Recipes
// BAD: Copy-paste same styles
const button = { _disabled: { opacity: 0.4 } }
const input = { _disabled: { opacity: 0.4 } }
// GOOD: Extract to shared constant
const disabledStyles = { opacity: 0.4, cursor: 'not-allowed' }
const button = { _disabled: disabledStyles }
const input = { _disabled: disabledStyles }
Accessing Official Panda CSS Docs
For recipe-specific documentation:
- Resolve library ID:
mcp__MCP_DOCKER__resolve-library-idwithlibraryName: "panda-css" - Fetch docs:
mcp__MCP_DOCKER__get-library-docswithtopic: "recipes"ortopic: "slot-recipes"
Next Steps
After creating recipes:
- Implement in components: Use panda-component-impl skill for React integration
- Test variants: Verify all combinations work visually
- Document in Storybook: Create stories showing all variants
Reference Files from Best Practices Repo
- Regular recipe:
src/recipes/button.ts - Slot recipe:
src/recipes/checkbox.ts,src/recipes/tooltip.ts,src/recipes/menu.ts - Shared base:
src/recipes/button.ts(buttonBase constant) - Dynamic variants:
src/recipes/text.ts(fontSize generation) - Config registration:
panda.config.ts,cetec-preset.ts