Skip to content

Component Architecture Decisions

When to Use

Use flat props for simple single-purpose UI. Use compound components when the caller needs to place named slots (header/body/footer) in their markup. Use headless (Radix primitive) when full styling freedom across themes is required.

Decision

If you need... Use... Why
Simple, single-purpose UI (button, badge, icon) Flat props component Minimal API; props are the natural contract
Multi-slot layout (card with header/body/footer) Compound components Slot placement stays in the caller's markup
Behavior + styling bundled Styled component with CVA variants One import; designer-friendly API
Behavior only, consumer supplies styling Headless component (Radix primitive) Full styling freedom; reuse across themes
Form input needing external control Controlled component with value/onChange Integrates with react-hook-form; single source of truth
Toggle/disclosure not needing parent state Uncontrolled with internal state + defaultValue Simpler caller code when parent doesn't own the state
Component renders as different HTML elements Polymorphic with as prop Correct semantics without wrapper divs

Pattern

Flat props (simple):

// Button — flat props, CVA variants
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
}
export function Button({ variant = 'primary', size = 'md', className, ...props }: ButtonProps) {
  return <button className={cn(buttonVariants({ variant, size }), className)} {...props} />;
}

Compound (card with slots):

// Card — compound pattern, slots via dot notation
export function Card({ children, className }: { children: React.ReactNode; className?: string }) {
  return <div className={cn('rounded-lg border bg-card', className)}>{children}</div>;
}
Card.Header = function CardHeader({ children }: { children: React.ReactNode }) {
  return <div className="flex flex-col space-y-1.5 p-6">{children}</div>;
};
Card.Body = function CardBody({ children }: { children: React.ReactNode }) {
  return <div className="p-6 pt-0">{children}</div>;
};

Common Mistakes

  • Wrong: Building a compound component when flat props would suffice → Right: Add compound pattern only when the caller's layout genuinely needs named slots
  • Wrong: Making everything controlled → Right: Use uncontrolled with defaultValue for trivial toggle/open state
  • Wrong: Mixing headless + styled concerns in one component → Right: Separate them so the component is reusable across themes
  • Wrong: Using render props where compound components now suffice → Right: Compound components with Context are the modern equivalent
  • Wrong: Choosing composition depth by internal complexity → Right: The complexity of the caller's layout needs drives compound vs flat

See Also