Props & API Standards

Component props define the interface between your design system and the teams using it. Well-designed props enable adoption, prevent misuse, and scale with complexity. Poorly designed props lead to prop explosion, inconsistent usage, and maintenance nightmares.

Why Props Matter

Props are the contract between components and consumers. They determine:

  • Usability: Can developers understand and use the component without reading implementation?
  • Flexibility: Does the API accommodate common use cases without requiring workarounds?
  • Consistency: Do similar components follow similar prop patterns?
  • Maintainability: Can the component evolve without breaking consumers?

Core Principles

1. Prop Names Should Describe Purpose, Not Implementation

// ❌ Bad: Implementation detail
<Button variant="blue" />

// ✅ Good: Purpose-driven
<Button variant="primary" />

Names should communicate intent, not how something is implemented. This allows implementation to evolve without breaking the API.

2. Follow Layer-Appropriate Patterns

Different component layers have different prop patterns:

  • Primitives: Minimal props, stable API, predictable behavior
  • Compounds: Props that coordinate sub-components, but avoid prop explosion
  • Composers: Use composition patterns (slots, render props, context) instead of many props

3. Use Semantic Types

// ❌ Bad: Generic types
size: 'small' | 'medium' | 'large'
variant: '1' | '2' | '3'

// ✅ Good: Semantic types
size: 'sm' | 'md' | 'lg'
variant: 'primary' | 'secondary' | 'ghost'

4. Provide Sensible Defaults

// ✅ Good: Defaults enable simple usage
interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'ghost'; // defaults to 'primary'
  size?: 'sm' | 'md' | 'lg'; // defaults to 'md'
  disabled?: boolean; // defaults to false
}

// Usage: Simple cases don't need all props
<Button>Click me</Button>

// Complex cases can still customize
<Button variant="ghost" size="sm" disabled>
  Cancel
</Button>

Common Prop Patterns

Variant Pattern

Use variants for visual style variations that are semantically different:

interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
}

// Variants communicate intent, not just appearance
<Button variant="primary">Save</Button>
<Button variant="danger">Delete</Button>
<Button variant="ghost">Cancel</Button>

Size Pattern

Use consistent size tokens across components:

interface ButtonProps {
  size?: 'sm' | 'md' | 'lg';
}

// Consistent size prop across components
<Button size="sm">Small</Button>
<Input size="sm" />
<Avatar size="sm" />

State Props

Use boolean props for binary states:

interface ButtonProps {
  disabled?: boolean;
  loading?: boolean;
  active?: boolean;
}

// Clear, predictable state management
<Button disabled={isSubmitting}>
  {isSubmitting ? 'Saving...' : 'Save'}
</Button>

Anti-Patterns & Pitfalls

1. Prop Explosion

Problem: Too many props make components hard to use and maintain.

// ❌ Bad: 15+ props
interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
  size?: 'sm' | 'md' | 'lg' | 'xl';
  icon?: React.ReactNode;
  iconPosition?: 'left' | 'right';
  loading?: boolean;
  loadingText?: string;
  disabled?: boolean;
  fullWidth?: boolean;
  rounded?: boolean;
  shadow?: boolean;
  uppercase?: boolean;
  // ... and more
}

// ✅ Good: Use composition instead
<Button variant="primary" icon={<Icon />} iconPosition="left">
  Save
</Button>

// Or extract to compound components
<ButtonGroup>
  <Button variant="primary">Save</Button>
  <Button variant="secondary">Cancel</Button>
</ButtonGroup>

Solution: Extract related props into compound components or use composition patterns.

2. Magic String Props

Problem: String literals without type safety lead to errors.

// ❌ Bad: No type safety
<Button variant="primay" /> // Typo! No error

// ✅ Good: TypeScript unions
type ButtonVariant = 'primary' | 'secondary' | 'ghost';
interface ButtonProps {
  variant?: ButtonVariant;
}

<Button variant="primay" /> // TypeScript error!

3. Props That Control Multiple Concerns

Problem: Single prop controlling multiple behaviors creates confusion.

// ❌ Bad: One prop controls multiple things
<Button mode="loading-primary-large" />

// ✅ Good: Separate concerns
<Button variant="primary" size="lg" loading />

4. Inconsistent Prop Names

Problem: Similar components use different prop names for the same concept.

// ❌ Bad: Inconsistent naming
<Button size="sm" />
<Input scale="sm" />
<Avatar dimension="sm" />

// ✅ Good: Consistent naming
<Button size="sm" />
<Input size="sm" />
<Avatar size="sm" />

Layer-Specific Guidelines

Primitives

Primitives should have minimal, stable props:

  • Keep props minimal: Only essential props, avoid convenience props
  • Stable API: Props should rarely change
  • Predictable behavior: Props should behave consistently
// ✅ Good primitive: Minimal, stable props
interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  onClick?: () => void;
  children: React.ReactNode;
}

Compounds

Compounds coordinate multiple primitives but avoid prop explosion:

  • Use composition: Allow children/props to configure sub-components
  • Avoid mega-props: Don't try to control every variation
  • Provide sensible defaults: Common cases should work out of the box
// ✅ Good compound: Coordinates without explosion
interface TextFieldProps {
  label?: string;
  error?: string;
  hint?: string;
  // Delegate to Input primitive
  inputProps?: InputProps;
}

// Simple usage
<TextField label="Email" />

// Advanced usage
<TextField 
  label="Email"
  inputProps={{ type: "email", placeholder: "Enter email" }}
/>

Composers

Composers should use composition patterns, not many props:

  • Use slots: Allow consumers to customize sub-parts
  • Use render props: For dynamic content
  • Use context: For shared state across children
// ✅ Good composer: Uses composition
interface ModalProps {
  isOpen: boolean;
  onClose: () => void;
  title?: React.ReactNode;
  children: React.ReactNode;
  footer?: React.ReactNode; // Slot for customization
}

<Modal isOpen={isOpen} onClose={handleClose} title="Confirm">
  <p>Are you sure?</p>
  <Modal.Footer> {/* Slot */}
    <Button onClick={handleClose}>Cancel</Button>
    <Button variant="primary">Confirm</Button>
  </Modal.Footer>
</Modal>

Documentation Standards

Required Documentation

Every prop should include:

  • Type: TypeScript type definition
  • Required: Whether the prop is required or optional
  • Default: Default value if optional
  • Description: What the prop does and when to use it
  • Examples: Code examples showing usage

JSDoc Comments

/**
 * Button component for user actions
 * 
 * @example
 * <Button variant="primary" onClick={handleClick}>
 *   Click me
 * </Button>
 */
interface ButtonProps {
  /**
   * Visual style variant
   * @default 'primary'
   */
  variant?: 'primary' | 'secondary' | 'ghost';
  
  /**
   * Size of the button
   * @default 'md'
   */
  size?: 'sm' | 'md' | 'lg';
  
  /**
   * Whether the button is disabled
   * @default false
   */
  disabled?: boolean;
  
  /**
   * Click handler
   */
  onClick?: () => void;
  
  /**
   * Button content
   */
  children: React.ReactNode;
}

Migration & Evolution

Adding New Props

When adding props:

  1. Make them optional with sensible defaults
  2. Document them thoroughly
  3. Add examples showing usage
  4. Consider if composition would be better

Deprecating Props

When deprecating props:

  1. Add deprecation warning in JSDoc
  2. Provide migration path
  3. Support both old and new API during transition
  4. Remove after grace period (e.g., 6 months)
interface ButtonProps {
  /**
   * @deprecated Use variant="primary" instead
   */
  primary?: boolean;
  
  /**
   * Visual style variant
   * @default 'primary'
   */
  variant?: 'primary' | 'secondary' | 'ghost';
}

// Component implementation
export function Button({ primary, variant, ...props }: ButtonProps) {
  // Support both during migration
  const finalVariant = variant || (primary ? 'primary' : undefined);
  
  if (primary) {
    console.warn('Button primary prop deprecated. Use variant="primary" instead.');
  }
  
  return <button className={variantStyles[finalVariant]} {...props} />;
}

Related Resources