React Architecture29-08-202411 min read

Compound Components Pattern in React

A practical guide to designing flexible, scalable component APIs using the compound components pattern, with clear examples and modern React architecture.


Compound Components Pattern in React


Compound components let you build flexible, expressive APIs by allowing multiple components to work together as a unified system. Instead of stuffing a single component with endless props, compound components rely on composition and shared context to create predictable, scalable interfaces.


This pattern is heavily used in real world component libraries because it keeps UI design flexible while centralizing logic in a clean, maintainable structure.




1. Why Compound Components Exist


A common problem in UI development is the overloaded component that tries to handle every possible variation through props.


Example of an inflexible API:


<Dropdown
  label="Menu"
  open={open}
  onOpenChange={setOpen}
  items={[{ label: "A" }, { label: "B", disabled: true }]}
  size="md"
  placement="bottom-end"
/>

This approach becomes hard to scale and even harder to customize.


Compound components solve this by letting the consumer declare the structure explicitly:


<Dropdown>
  <Dropdown.Trigger>Menu</Dropdown.Trigger>
  <Dropdown.Menu>
    <Dropdown.Item>A</Dropdown.Item>
    <Dropdown.Item disabled>B</Dropdown.Item>
  </Dropdown.Menu>
</Dropdown>

This design is:


  • more readable
  • more customizable
  • easier to maintain
  • more scalable as features are added



2. The Core Idea: Shared Context


Compound components work by sharing context between a parent and its children.


The parent owns the logic.

The children handle the UI.


Step 1: Create a context and parent component


import { createContext, useContext, useState } from "react";

const DropdownContext = createContext(null);

function Dropdown({ children }) {
  const [open, setOpen] = useState(false);

  const value = { open, setOpen };

  return (
    <DropdownContext.Provider value={value}>
      <div className="dropdown">{children}</div>
    </DropdownContext.Provider>
  );
}

Step 2: Child components consume that context


function Trigger({ children }) {
  const { open, setOpen } = useContext(DropdownContext);

  return (
    <button onClick={() => setOpen(!open)}>
      {children} {open ? "▲" : "▼"}
    </button>
  );
}

function Menu({ children }) {
  const { open } = useContext(DropdownContext);

  if (!open) return null;

  return <ul className="dropdown-menu">{children}</ul>;
}

Step 3: Attach child components to the parent


Dropdown.Trigger = Trigger;
Dropdown.Menu = Menu;

Now consumers can build any UI they want using these primitives.




3. Benefits of Compound Components


1. Clear and expressive APIs

Consumers declare intent through structure rather than props.


2. Easy to scale

Adding features does not create prop explosion. Just add new child components.


3. Highly customizable

Consumers choose exactly how the UI is shaped.


4. Centralized logic

The parent manages behavior.

Children stay lightweight and focused.




4. When to Use Compound Components


Use this pattern for any UI where multiple parts work in coordination:


  • tabs
  • dropdown menus
  • modals
  • steppers
  • accordions
  • carousels
  • navigation sections

If you ever find yourself creating a component with more than five props, consider whether a compound structure would be more maintainable.




5. Adding Features: Disabled Items, Close Behavior, Keys


You can enhance behavior inside the child components while preserving flexibility.


Example: Closing the menu when an item is clicked


function Item({ children, disabled }) {
  const { setOpen } = useContext(DropdownContext);

  return (
    <li
      className={disabled ? "disabled" : ""}
      onClick={() => {
        if (!disabled) setOpen(false);
      }}
    >
      {children}
    </li>
  );
}

Attach it:


Dropdown.Item = Item;

Consumers now have a complete, flexible dropdown system.




6. Controlled vs Uncontrolled Modes


Compound components work in both modes.


Uncontrolled


<Dropdown>
  <Dropdown.Trigger>Menu</Dropdown.Trigger>
  <Dropdown.Menu>
    <Dropdown.Item>A</Dropdown.Item>
  </Dropdown.Menu>
</Dropdown>

Controlled


You manage the state from outside:


const [open, setOpen] = useState(false);

<Dropdown open={open} onOpenChange={setOpen}>
  <Dropdown.Trigger>Menu</Dropdown.Trigger>
  <Dropdown.Menu>
    <Dropdown.Item>A</Dropdown.Item>
  </Dropdown.Menu>
</Dropdown>;

This equips the component for real world usage in design systems.




7. Anti Patterns to Avoid


1. Passing state through props instead of context

This breaks the entire purpose of the pattern.


2. Hardcoding layout inside the parent

Let consumers control layout. Keep logic central, UI flexible.


3. Mixing logic and presentation

Keep logic in the parent or hooks.

Keep structural decisions in child components.




8. Example: Tab System with Compound Components


A more advanced but practical example:


const TabsContext = createContext(null);

function Tabs({ defaultIndex = 0, children }) {
  const [index, setIndex] = useState(defaultIndex);

  return (
    <TabsContext.Provider value={{ index, setIndex }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

function TabList({ children }) {
  return <div className="tab-list">{children}</div>;
}

function Tab({ children, tabIndex }) {
  const { index, setIndex } = useContext(TabsContext);

  return (
    <button
      className={index === tabIndex ? "active" : ""}
      onClick={() => setIndex(tabIndex)}
    >
      {children}
    </button>
  );
}

function TabPanel({ children, panelIndex }) {
  const { index } = useContext(TabsContext);

  return index === panelIndex ? <div>{children}</div> : null;
}

Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;

Consumers now get a clean, scalable API:


<Tabs defaultIndex={0}>
  <Tabs.List>
    <Tabs.Tab tabIndex={0}>Overview</Tabs.Tab>
    <Tabs.Tab tabIndex={1}>Details</Tabs.Tab>
  </Tabs.List>

  <Tabs.Panel panelIndex={0}>Overview content</Tabs.Panel>
  <Tabs.Panel panelIndex={1}>Details content</Tabs.Panel>
</Tabs>



Final Thoughts


Compound components are one of the most powerful patterns for building reusable, scalable UI systems in React.


They promote:


  • clear structure
  • centralized behavior
  • flexible layout
  • clean composition
  • maintainability at scale

Mastering this pattern elevates your component design and aligns your work with modern design system architecture.