876 lines
20 KiB
Markdown
876 lines
20 KiB
Markdown
---
|
|
name: panda-recipe-patterns
|
|
description: 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`
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
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`
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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`
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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`:
|
|
|
|
```typescript
|
|
// 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`:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
1. **Resolve library ID**: `mcp__MCP_DOCKER__resolve-library-id` with `libraryName: "panda-css"`
|
|
2. **Fetch docs**: `mcp__MCP_DOCKER__get-library-docs` with `topic: "recipes"` or `topic: "slot-recipes"`
|
|
|
|
## Next Steps
|
|
|
|
After creating recipes:
|
|
|
|
1. **Implement in components**: Use **panda-component-impl** skill for React integration
|
|
2. **Test variants**: Verify all combinations work visually
|
|
3. **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`
|