Detail chart · Frontend

Component Composition

Pattern ◆◇◇◇◇

Build complex UI from small, focused components that compose cleanly rather than inheriting behaviour.

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.

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

Avoid when:

Trade-offs

BenefitCost
Components remain small and single-purposeCallers must understand the composition API
Structural changes to the parent don’t ripple into contentProp drilling risk if context is not used for deep trees
Each piece is independently testable and reusableOver-composing trivial UI creates unnecessary indirection
Design system consumers can extend without forkingCompound 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.