18 KiB
name, description
| name | description |
|---|---|
| panda-component-impl | Build React components that properly use Panda CSS patterns, recipes, TypeScript integration, and accessibility best practices |
Panda CSS Component Implementation
When to Use This Skill
Use this skill when:
- Building React components with Panda CSS styling
- Implementing recipe-based component variants
- Creating polymorphic components (components that can render as different elements)
- Integrating TypeScript with Panda CSS types
- Implementing accessible components with Panda CSS
- Setting up component file structure
For creating the recipes themselves, use the panda-recipe-patterns skill first.
Component File Structure
src/
components/
Button/
Button.tsx # Component implementation
index.tsx # Public exports
Button.stories.tsx # Storybook documentation (optional)
Icon/
Icon.tsx
index.tsx
svg/ # SVG source files (for icon systems)
CheckBox/
CheckBox.tsx
index.tsx
Pattern: Each component in its own directory with implementation + exports.
Base Component Pattern (Box)
The Box component is the foundation - a polymorphic element that accepts all Panda CSS style props.
Create: src/components/Box/Box.tsx
import { createElement } from 'react'
import type { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react'
import { cx } from '@styled-system/css'
import { box } from '@styled-system/patterns'
import type { SystemStyleObject } from '@styled-system/types'
import { splitProps } from '~/utils/splitProps'
// Box can render as any HTML element
export type BoxProps =
Omit<ComponentPropsWithoutRef<ElementType>, 'as'> &
SystemStyleObject & // Enable all Panda CSS style props
{
as?: ElementType
children?: ReactNode
}
export const Box = ({ as = 'div', ...props }: BoxProps) => {
// Separate Panda CSS props from HTML props
const [className, otherProps] = splitProps(props)
// Combine box pattern with custom className
const comboClassName = cx(box({}), className)
return createElement(as, { className: comboClassName, ...otherProps })
}
Key Points:
asprop: Polymorphic rendering (div, button, a, span, etc.)SystemStyleObject: Enables all Panda CSS style props (bg, px, fontSize, etc.)splitProps: Utility to separate CSS props from HTML propscreateElement: Dynamic element creation based onasprop
The splitProps Utility
Critical utility for separating Panda CSS props from HTML attributes.
Create: src/utils/splitProps.ts
import { cx, css, splitCssProps } from '@styled-system/css'
/**
* Splits component props into Panda CSS props and HTML props.
* Returns [className, otherProps].
*/
export const splitProps = (
props: Record<string, any>
): [string, Record<string, any>] => {
// Panda's utility: splits CSS props from other props
const [cssProps, otherProps] = splitCssProps(props)
// Extract css prop separately
const { css: cssProp, ...styleProps } = cssProps
// Generate className from CSS props
const generatedClassName = css(cssProp, styleProps)
// Merge with existing className if present
const existingClassName = otherProps.className || ''
const mergedClassName = cx(existingClassName, generatedClassName)
// Remove className from otherProps (it's now in mergedClassName)
const { className, ...remainingProps } = otherProps
return [mergedClassName, remainingProps]
}
Why: Enables inline style props on components while keeping clean HTML output.
Usage:
// Component receives both CSS and HTML props
<Button bg="blue.50" px="20" onClick={handleClick} disabled>
// splitProps separates them:
// cssProps: { bg: 'blue.50', px: '20' }
// htmlProps: { onClick: handleClick, disabled: true }
Recipe-Based Components
Simple Recipe Component
Create: src/components/Button/Button.tsx
import { type FC } from 'react'
import { cx } from '@styled-system/css'
import { button, type ButtonVariantProps } from '@styled-system/recipes'
import { Box, type BoxProps } from '../Box/Box'
import { splitProps } from '~/utils/splitProps'
export type ButtonProps =
BoxProps &
ButtonVariantProps &
{
loading?: boolean
disabled?: boolean
href?: string
}
export const Button: FC<ButtonProps> = ({
variant,
size,
loading = false,
disabled = false,
href,
...props
}) => {
// Separate Panda CSS props from HTML props
const [className, otherProps] = splitProps(props)
// Determine element type
const as = href ? 'a' : 'button'
// Combine recipe className with custom className
const comboClassName = cx(
button({ variant, size }), // Recipe styles
className // Custom overrides
)
return (
<Box
as={as}
className={comboClassName}
disabled={loading || disabled}
href={href}
{...otherProps}
/>
)
}
Pattern Breakdown:
- Import recipe and its variant types from
@styled-system/recipes - Extend
BoxPropswithButtonVariantPropsfor full type safety - Use
splitPropsto separate CSS from HTML props - Apply recipe with
button({ variant, size }) - Merge recipe className with custom className using
cx - Pass to Box component for rendering
Slot Recipe Component
Multi-part components use slot recipes.
Create: src/components/CheckBox/CheckBox.tsx
import { type FC, type InputHTMLAttributes } from 'react'
import { checkbox, type CheckboxVariantProps } from '@styled-system/recipes'
import { Box } from '../Box/Box'
import { Icon } from '../Icon/Icon'
export type CheckBoxProps =
Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> &
CheckboxVariantProps &
{
label?: string
indeterminate?: boolean
error?: boolean
}
export const CheckBox: FC<CheckBoxProps> = ({
size,
label,
indeterminate = false,
error = false,
checked,
...props
}) => {
// Get slot class names from recipe
const { container, input, indicator } = checkbox({ size })
return (
<Box as="label" className={container}>
<Box
as="input"
type="checkbox"
className={input}
checked={checked}
// Data attributes for custom states
{...(indeterminate && { 'data-indeterminate': true })}
{...(error && { 'data-error': true })}
{...props}
/>
{/* Different icons for different states */}
<Icon className={indicator} name="checkbox" data-state="unchecked" />
<Icon className={indicator} name="checkbox-checked" data-state="checked" />
<Icon className={indicator} name="checkbox-indeterminate" data-state="indeterminate" />
{label && (
<Box as="span" className={checkbox().label}>
{label}
</Box>
)}
</Box>
)
}
Slot Recipe Pattern:
- Destructure slot classes:
{ container, input, indicator } - Apply each slot class to corresponding element
- Use data attributes for custom states:
data-indeterminate,data-error - Recipe CSS targets these data attributes via conditions
Component with Conditional Rendering
Create: src/components/Button/Button.tsx (with loading state)
import { type FC, type ReactNode } from 'react'
import { cx } from '@styled-system/css'
import { button, type ButtonVariantProps } from '@styled-system/recipes'
import { Box, type BoxProps } from '../Box/Box'
import { Spinner } from '../Spinner/Spinner'
import { splitProps } from '~/utils/splitProps'
export type ButtonProps =
BoxProps &
ButtonVariantProps &
{
loading?: boolean
leftIcon?: ReactNode
rightIcon?: ReactNode
children: ReactNode
}
export const Button: FC<ButtonProps> = ({
variant,
size,
loading = false,
disabled = false,
leftIcon,
rightIcon,
children,
...props
}) => {
const [className, otherProps] = splitProps(props)
return (
<Box
as="button"
className={cx(button({ variant, size }), className)}
disabled={loading || disabled}
aria-busy={loading} // Accessibility: announce loading state
{...otherProps}
>
{/* Show spinner when loading */}
{loading && <Spinner size={size} />}
{/* Show left icon if not loading */}
{!loading && leftIcon}
{/* Button text */}
<span>{children}</span>
{/* Right icon */}
{!loading && rightIcon}
</Box>
)
}
TypeScript Patterns
Extract Recipe Types
import { button, type ButtonVariantProps } from '@styled-system/recipes'
// ButtonVariantProps includes:
// - variant?: 'primary' | 'secondary' | 'outline' | 'ghost'
// - size?: 'small' | 'medium' | 'large'
Pattern: Always use generated variant types for prop types.
Omit Conflicting Props
import { text, type TextVariantProps } from '@styled-system/recipes'
// Avoid prop conflicts between Box and recipe
export type TextProps =
Omit<BoxProps, keyof TextVariantProps> & // Remove conflicts
TextVariantProps &
{
children: ReactNode
}
Why: Prevents TypeScript errors when Box and recipe define same props.
Conditional Value Types
For responsive/theme-aware props:
import { type ConditionalValue } from '@styled-system/types'
import { type ColorToken } from '@styled-system/tokens'
export type IconProps = {
fill?: ConditionalValue<ColorToken> // Enables: fill="blue.50" or fill={{ base: 'blue.50', _dark: 'blue.40' }}
}
Component Props Pattern
import { type ComponentPropsWithoutRef } from 'react'
// Get all props for a specific HTML element
export type InputProps = ComponentPropsWithoutRef<'input'> & {
// Custom props
}
// For polymorphic components
export type BoxProps = ComponentPropsWithoutRef<ElementType> & {
as?: ElementType
}
Accessibility Patterns
Always Include focusVisible
In recipes:
base: {
_focusVisible: {
outlineWidth: '2',
outlineOffset: '1',
outlineColor: { base: 'blue.50', _dark: 'blue.40' }
}
}
Why: Provides visible focus indication for keyboard navigation.
ARIA Attributes
export const Button: FC<ButtonProps> = ({ loading, disabled, ...props }) => {
return (
<button
disabled={loading || disabled}
aria-disabled={disabled}
aria-busy={loading}
{...props}
/>
)
}
Common ARIA Attributes:
aria-label: Label for screen readersaria-disabled: Disabled statearia-busy: Loading statearia-checked: Checkbox/radio statearia-expanded: Collapsed/expanded statearia-pressed: Toggle button state
Keyboard Interaction
export const MenuItem: FC<MenuItemProps> = ({ onClick, ...props }) => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === ' ' || e.key === 'Spacebar' || e.key === 'Enter') {
e.preventDefault()
onClick?.(e)
}
}
return (
<Box
role="button"
tabIndex={0}
onKeyDown={handleKeyDown}
onClick={onClick}
{...props}
/>
)
}
Why: Ensure keyboard users can interact with custom components.
Match Multiple State Selectors
In recipes, support both native and custom states:
conditions: {
checked: '&:is(:checked, [data-checked], [aria-checked=true], [data-state="checked"])'
}
Why: Works with native inputs AND custom components.
Icon Component Pattern
Icons often need special handling for sizing and color.
Create: src/components/Icon/Icon.tsx
import { type FC } from 'react'
import { cx } from '@styled-system/css'
import { icon } from '@styled-system/patterns'
import type { ConditionalValue } from '@styled-system/types'
import type { ColorToken } from '@styled-system/tokens'
import { Box, type BoxProps } from '../Box/Box'
import { splitProps } from '~/utils/splitProps'
import { numericSizes } from '~/styles/tokens'
// Constrain size to numeric tokens only
export type AllowedIconSizes = keyof typeof numericSizes
export type IconProps =
Omit<BoxProps, 'size'> & // Remove BoxProps size
{
name: string // Icon identifier
size?: AllowedIconSizes
fill?: ConditionalValue<ColorToken>
}
export const Icon: FC<IconProps> = ({
name,
size = '24',
fill = 'currentColor',
...props
}) => {
const [className, otherProps] = splitProps(props)
return (
<Box
as="svg"
viewBox="0 0 24 24"
className={cx(icon({ size }), className)} // icon pattern sets width + height
fill="none"
stroke={fill === 'currentColor' ? fill : undefined}
{...otherProps}
>
{/* SVG sprite reference */}
<use xlinkHref={`/sprite.svg#${name}`} />
</Box>
)
}
Pattern: Custom icon pattern enforces square sizing via tokens.
Responsive Components
Responsive Props
Use object syntax for breakpoint-based props:
<Box
px={{ base: '16', md: '20', lg: '24' }}
fontSize={{ base: 'sm', md: 'md' }}
display={{ base: 'block', lg: 'flex' }}
/>
Container Queries
For component-level responsive design:
<Box
containerType="inline-size" // Enable container queries
width="full"
>
<Box
// Responsive based on CONTAINER size, not viewport
p={{ base: '12', '@container(min-width: 400px)': '20' }}
/>
</Box>
Component Composition
Compose with Box
export const Card: FC<BoxProps> = (props) => {
return (
<Box
bg={{ base: 'white', _dark: 'slate.90' }}
borderRadius="8"
boxShadow="md"
p="20"
{...props} // Allow overrides
/>
)
}
Pattern: Provide sensible defaults, allow prop overrides.
Compound Components
// Menu.tsx
export const Menu: FC<MenuProps> = ({ children, ...props }) => {
const { container } = menu()
return <Box className={container} {...props}>{children}</Box>
}
// MenuItem.tsx
export const MenuItem: FC<MenuItemProps> = ({ children, ...props }) => {
const { item } = menu() // Access same recipe
return <Box className={item} {...props}>{children}</Box>
}
// Usage
<Menu>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
</Menu>
Best Practices Checklist
Create TodoWrite items when building components:
- Use Box as foundation for polymorphic components
- Apply splitProps to separate CSS from HTML props
- Import and use recipe variant types for TypeScript
- Include ARIA attributes for accessibility
- Add keyboard interaction for custom interactive elements
- Use _focusVisible for visible focus states
- Test component in light AND dark themes
- Validate all variant combinations work correctly
- Test with keyboard-only navigation
- Test with screen reader (basic check)
Common Pitfalls
Avoid: Mixing CSS Approaches
// BAD: Mixing inline styles, Panda props, and classes
<Box
style={{ backgroundColor: 'red' }} // Inline style (avoid)
bg="blue.50" // Panda CSS (good)
className="custom-class" // External CSS (avoid)
/>
// GOOD: Use Panda CSS exclusively
<Box bg="blue.50" px="20" />
Avoid: Not Using Recipe Types
// BAD: Manual prop types (out of sync with recipe)
type ButtonProps = {
variant?: 'primary' | 'secondary'
}
// GOOD: Use generated types
import { type ButtonVariantProps } from '@styled-system/recipes'
type ButtonProps = ButtonVariantProps
Avoid: Missing Accessibility
// BAD: No keyboard support, no ARIA
<div onClick={handleClick}>Click me</div>
// GOOD: Proper semantics and keyboard support
<button onClick={handleClick} aria-label="Action button">
Click me
</button>
Avoid: Over-wrapping
// BAD: Unnecessary div wrappers
<Box>
<Box>
<Box>Content</Box>
</Box>
</Box>
// GOOD: Minimal, semantic structure
<Box>Content</Box>
Accessing Official Panda CSS Docs
For component implementation patterns:
- Resolve library ID:
mcp__MCP_DOCKER__resolve-library-idwithlibraryName: "panda-css" - Fetch docs:
mcp__MCP_DOCKER__get-library-docswith:topic: "recipes"- Using recipes in componentstopic: "typescript"- TypeScript patternstopic: "patterns"- Built-in patterns
Exporting Components
Create: src/components/Button/index.tsx
export { Button } from './Button'
export type { ButtonProps } from './Button'
Create: src/index.ts (main library export)
// Components
export { Box } from './components/Box'
export { Button, IconButton } from './components/Button'
export { Icon } from './components/Icon'
export { CheckBox } from './components/CheckBox'
// ... more components
// Types
export type { BoxProps } from './components/Box'
export type { ButtonProps, IconButtonProps } from './components/Button'
export type { IconProps } from './components/Icon'
// ... more types
Pattern: Export both components and their prop types for consuming projects.
Working Examples
Reference these files in the examples/ directory for production-tested patterns:
Utility Functions:
examples/utils/splitProps.ts- CSS/HTML prop separation utility// Separates Panda CSS props from HTML props const [className, htmlProps] = splitProps(props); // Uses splitCssProps, css(), and cx() to merge classNamesexamples/utils/ThemeContext.tsx- Theme provider with localStorage persistence// Manages light/dark theme with system preference detection // Persists user preference to localStorage // Applies theme class to document root
Token & Configuration Integration:
examples/preset.ts- Shows how to integrate tokens with components via presetexamples/conditions.ts- Custom conditions for component statesexamples/textStyles.ts- Typography presets for text components
For Complete Component Examples: While this plugin focuses on architecture patterns, you can reference:
- The skills themselves (this file, panda-recipe-patterns) contain inline component examples
- Use the panda-architect agent for full component implementations