Component Architecture Decisions
When to Use
When starting a new component or evaluating how to split an existing one. These decisions affect composability, API surface, and long-term maintainability.
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
- Building a compound component when flat props would suffice → adds boilerplate with no flexibility gain
- Making everything controlled → forces callers to manage trivial state (open/closed toggles)
- Mixing headless + styled concerns in one component → limits reuse across themes; separate them
- Using render props where compound components now suffice → render props are a React 2018 pattern; compound components with Context are the modern equivalent
- Choosing composition depth by component complexity alone → complexity of the caller's layout needs drives compound vs flat, not internal complexity
See Also
- Props Patterns
- Reference: shadcn/ui Button
- Reference: shadcn/ui Card
- Reference: Radix UI Primitives
- Reference: Compound Pattern — patterns.dev