Skip to main content

Compound Components

Compound components are sub-components that coordinate through shared context, giving consumers a flexible declarative API for composite widgets like tabs and accordions. They keep the public API small at the cost of more implementation machinery.

Type
Structural
When to Use
Build Flexible Widget API, Share Implicit State, Expose Declarative Markup, Avoid Config Prop Explosion

Compound components are a group of components designed to be used together, where a parent coordinates shared state and children consume it implicitly. Instead of configuring a widget through a long list of props, consumers compose child components declaratively. The classic analogy is HTML's <select> and <option>: each is meaningless alone but together they form a control.

How It Works

The parent owns the state and exposes related sub-components, typically attached as static properties (Tabs.List, Tabs.Tab, Tabs.Panel). It shares state with descendants through React context rather than explicit props, so children at any depth can read the active tab and dispatch changes without prop drilling. Earlier implementations used React.Children cloning to inject props, but context is the modern, depth-independent approach.

A consumer writes natural markup: a <Tabs> wrapping a <Tabs.List> of <Tabs.Tab> items and matching <Tabs.Panel>s. The parent decides which panel is visible; the children just declare structure.

When to Use It

Use compound components for composite UI controls with several interrelated parts — tabs, accordions, menus, dropdowns, form fields, dialogs — especially when building a reusable component library. The pattern shines when consumers need flexibility in layout and ordering that a single configuration-prop API cannot express cleanly. It keeps the public API small while allowing rich composition.

Trade-offs

The implementation is more involved than a single prop-driven component: you must design the context, the sub-components, and clear rules about which children are valid. Misuse — placing children outside the provider — fails at runtime unless you guard against a missing context. Documentation matters because the relationships between parts are implicit. For a simple, fixed widget the extra machinery is unjustified.

Related Patterns

Compound components rely on the provider pattern to distribute shared state. They are an alternative to render props for giving consumers control over structure, and they pair with custom hooks (e.g. a useTabsContext helper) to expose the shared state safely. The container/presentational split can coexist, with the parent acting as the stateful container.

Example

const TabsContext = createContext();
function Tabs({ children, defaultIndex = 0 }) {
  const [active, setActive] = useState(defaultIndex);
  return <TabsContext.Provider value={{ active, setActive }}>{children}</TabsContext.Provider>;
}
Tabs.Tab = function Tab({ index, children }) {
  const { active, setActive } = useContext(TabsContext);
  return <button aria-selected={active === index} onClick={() => setActive(index)}>{children}</button>;
};
Tabs.Panel = function Panel({ index, children }) {
  const { active } = useContext(TabsContext);
  return active === index ? <div>{children}</div> : null;
};

Consumers compose <Tabs>, <Tabs.Tab>, and <Tabs.Panel> freely while the parent silently manages the active index.