Summary
Component Composition is the practice of combining small, single-purpose components into larger UI structures through nesting and prop-passing rather than inheritance or monolithic components. It is the foundational design principle behind scalable frontend architectures: each component does one thing well, and complexity emerges from their arrangement.
Problem
As UI requirements grow, components accumulate responsibilities. A UserCard gains an avatar, a follow button, a notification badge, and inline editing. What began as 30 lines becomes 300, and changing one concern risks breaking others.
- How do you share layout structure without duplicating markup across variants?
- How do you let callers control specific regions of a component without threading dozens of props?
- How do you build a design system where consumers can compose UI without forking internals?
Solution
Compose components using children, named slots, or render props. The parent component defines structure and layout; the caller supplies the content for each region.
// ❌ Monolithic — caller can't customise without adding props
function UserCard({ name, avatar, action, badge }) {
return (
<div className="card">
<img src={avatar} />
<span>{badge}</span>
<h3>{name}</h3>
<button>{action}</button>
</div>
);
}
// ✅ Composed — structure is stable, regions are caller-supplied
function Card({ header, body, footer }: {
header: React.ReactNode;
body: React.ReactNode;
footer?: React.ReactNode;
}) {
return (
<div className="card">
<div className="card__header">{header}</div>
<div className="card__body">{body}</div>
{footer && <div className="card__footer">{footer}</div>}
</div>
);
}
// Caller assembles the full card from focused pieces
<Card
header={<UserAvatar src={user.avatar} badge={user.unread} />}
body={<UserBio name={user.name} title={user.title} />}
footer={<FollowButton userId={user.id} />}
/>
Compound Components
For tightly related UI families (tabs, accordions, form fields), expose a parent plus named child components that share implicit context:
// Compound component pattern — consumers write expressive JSX
<Tabs defaultTab="overview">
<Tabs.List>
<Tabs.Tab id="overview">Overview</Tabs.Tab>
<Tabs.Tab id="patterns">Patterns</Tabs.Tab>
</Tabs.List>
<Tabs.Panel id="overview"><OverviewContent /></Tabs.Panel>
<Tabs.Panel id="patterns"><PatternList /></Tabs.Panel>
</Tabs>
The Tabs parent owns state; Tab and Panel children consume it via context. Each component is individually testable, and the caller controls the shape of the UI.
When to Use
- Any component that must support multiple visual variants without a prop explosion (
size,variant,withIcon,withBadge, …) - Design system primitives: buttons, cards, modals, form fields — regions must be open for extension
- Dashboard layouts where different pages share chrome (sidebar, header) but own their content regions
- Component libraries shipped to multiple teams who will supply their own content
Avoid when:
- The component has a single, invariant rendering — composability adds complexity without value
- Deeply nested composition trees obscure data flow; consider co-locating state instead
Trade-offs
| Benefit | Cost |
|---|---|
| Components remain small and single-purpose | Callers must understand the composition API |
| Structural changes to the parent don’t ripple into content | Prop drilling risk if context is not used for deep trees |
| Each piece is independently testable and reusable | Over-composing trivial UI creates unnecessary indirection |
| Design system consumers can extend without forking | Compound component context adds internal complexity |
Live Playground
Experiment with the pattern below. The two tabs show a monolithic UserCard component and the refactored composed equivalent. Edit either file to see how composition enables flexible UI assembly without the caller drowning in props.
Related Patterns
- Micro-Frontend Architecture — composition at the application boundary; the same principle applied at a larger scale
- State Management Patterns — shared state across composed components often requires lifting state or context
- Single Responsibility — component composition is SRP applied to UI: one component, one concern
- Composition Over Inheritance — the OOP principle that motivates this approach in the UI layer
- Dependency Injection — slot-based and render-prop composition are the UI analog of DI: content and behavior are passed in from outside rather than hardcoded