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.