While studying through the curso.dev project, I came across an interesting pattern while building parts of the UI with GitHub Primer: the Composition Pattern.

It’s one of those patterns that feels almost obvious once you understand it. Instead of designing components that try to anticipate every possible use case through an ever-growing list of props, composition lets us build interfaces from smaller pieces that work together.

In this post, I want to explore why this approach is useful, how it works in React, and how it appears in components from GitHub Primer.

Motivation

When building components in React, it’s common to start by putting most of the component’s behavior into props.

Imagine a simple Card:

<Card
  title="My profile"
  description="User information"
  showFooter
  footerText="Edit profile"
  showAvatar
  avatar="/avatar.png"
/>

At first, this works perfectly well. The problem appears as the component evolves.

What if we want to add an icon to the title? What if the footer needs two buttons instead of one? What if we need some extra content between the title and the description?

One possible solution is to keep adding props:

<Card
  title="My profile"
  description="..."
  icon={...}
  avatar={...}
  footer={...}
  showFooter
  ...
/>

This works, but the component gradually becomes responsible for more and more scenarios. Its API grows, conditional logic starts accumulating internally, and changing the component becomes increasingly difficult.

This is where the Composition Pattern becomes useful.

The idea behind the Composition Pattern

Composition is essentially about building something larger from smaller parts.

Applied to React components, we can summarize the idea like this:

Instead of creating a component that needs to know about every possible scenario, we create smaller components that can be combined.

React already gives us a simple mechanism for doing this through children:

<Card>
  <h2>John Doe</h2>
  <p>Software Engineer</p>
  <Button>Save</Button>
</Card>

Now, the Card doesn’t need to know what kind of content it contains:

function Card({ children }) {
  return <div className="card">{children}</div>;
}

Its responsibility is simply to provide the container.

The component using Card decides what belongs inside it.

That small change moves an important decision away from the component itself. Instead of trying to predict every possible variation, the component provides a structure that can be composed by whoever uses it.

Going beyond children

For more complex components, we can take this idea further by creating smaller components that represent specific parts of the structure.

For example:

<Card>
  <Card.Header>
    <Card.Title>John Doe</Card.Title>
    <Card.Description>Software Engineer</Card.Description>
  </Card.Header>

  <Card.Content>Some information...</Card.Content>

  <Card.Footer>
    <Button>Cancel</Button>
    <Button>Save</Button>
  </Card.Footer>
</Card>

Conceptually, the component now looks like this:

Card
├── Header
│   ├── Title
│   └── Description
├── Content
└── Footer

Each part has a clear responsibility, but none of them needs to assume that all the others will always exist.

For example, we could render only the content:

<Card>
  <Card.Content>Content</Card.Content>
</Card>

Or combine a header and content:

<Card>
  <Card.Header>
    <Card.Title>Profile</Card.Title>
  </Card.Header>

  <Card.Content>Content</Card.Content>
</Card>

There is no need for props such as showHeader, showFooter, or showDescription.

The structure itself expresses what the component should contain.

What about props?

Using the Composition Pattern doesn’t mean avoiding props.

Props and composition solve different problems.

A useful way to think about the distinction is:

Props configure. Composition defines structure.

Consider a button:

<Button variant="primary" size="sm">
  Save
</Button>

Here, variant and size configure how the button behaves or looks. Props are a natural fit.

But when we’re defining the structure of something larger:

<Card>
  <Card.Header>
    <Card.Title>Profile</Card.Title>
  </Card.Header>

  <Card.Content>...</Card.Content>

  <Card.Footer>...</Card.Footer>
</Card>

composition gives us much more flexibility.

Rather than making the parent component responsible for every possible combination, we let its smaller parts define the final structure.

Composition Pattern in GitHub Primer

One of the places where I started paying more attention to this pattern was while working on the curso.dev project.

The project uses GitHub Primer for its interface, and several Primer components expose APIs based heavily on composition.

A good example is Header:

<Header>
  <Header.Item full={true}>
    <Header.Link href="/">FinTab</Header.Link>
  </Header.Item>

  <Header.Item>
    <Header.Link href="/login">Login</Header.Link>
  </Header.Item>

  <Header.Item>
    <Header.Link href="/signup">Sign up</Header.Link>
  </Header.Item>
</Header>

Notice what the Header doesn’t do.

It doesn’t expose an API like this:

<Header logo="FinTab" loginLink="/login" signupLink="/signup" />

Instead, Primer gives us smaller building blocks:

  • Header defines the main container;
  • Header.Item represents an item inside it;
  • Header.Link represents a link.

We decide which items exist and how they should be combined.

Conceptually, the structure looks like this:

Header
├── Header.Item
│   └── Header.Link
├── Header.Item
│   └── Header.Link
└── Header.Item
    └── Header.Link

Now imagine that we want to add another navigation item.

We don’t need to change the Header API or introduce another prop. We simply change its composition:

<Header>
  <Header.Item full={true}>
    <Header.Link href="/">FinTab</Header.Link>
  </Header.Item>

  <Header.Item>
    <Header.Link href="/about">About</Header.Link>
  </Header.Item>

  <Header.Item>
    <Header.Link href="/login">Login</Header.Link>
  </Header.Item>
</Header>

The component itself hasn’t changed.

Only its composition has.

Another example: PageLayout

Another Primer component I encountered while working on the curso.dev project is PageLayout.

Consider this example:

<PageLayout>
  <PageLayout.Content width={contentWidth} className={extraContentClassName}>
    {children}
  </PageLayout.Content>

  <PageLayout.Footer divider="line">
    <Text size="small">&#169; {new Date().getFullYear()} FinTab</Text>
  </PageLayout.Footer>
</PageLayout>

Again, PageLayout doesn’t need a long list of props describing everything that should appear on the page.

Instead, it provides smaller parts that can be combined:

PageLayout
├── PageLayout.Content
│   └── page content
└── PageLayout.Footer
    └── footer content

PageLayout provides the overall structure, while we decide which parts we actually need and what should go inside them.

For example, a page may only need content:

<PageLayout>
  <PageLayout.Content>{children}</PageLayout.Content>
</PageLayout>

Another page might also need a footer:

<PageLayout>
  <PageLayout.Content>{children}</PageLayout.Content>

  <PageLayout.Footer>
    <Text>FinTab</Text>
  </PageLayout.Footer>
</PageLayout>

The important part is that PageLayout doesn’t need to anticipate these variations beforehand.

It provides the pieces. We decide how to assemble them.

Final thoughts

What I like about the Composition Pattern is that it changes the question we ask when designing a component.

Instead of asking:

“Which props do I need to support every possible use case?”

we can ask:

“Which smaller pieces should this component expose so they can be combined?”

That doesn’t mean every component needs to become a collection of nested components. For many cases, a few well-designed props are still the simplest solution.

But as a component grows and starts accumulating flags, conditional rendering, and props that exist only to support specific layouts, composition becomes an interesting alternative.

Working with GitHub Primer while building the curso.dev project made this idea much more concrete for me. Components like Header and PageLayout show how an API can remain flexible without trying to predict every possible way it will be used.