Compound Components: The Design Pattern That Changed How I Build UIs

Most component APIs grow boolean props until they're unreadable. Compound components are the pattern that stops that from happening — by making the component's structure part of its API. Here's how it works and when to reach for it.
Here's a component API that starts reasonably:
Six months later, after feature requests:
Every new requirement became a new boolean prop. The component is now doing seventeen things and the API communicates none of them clearly. Adding an eighteenth feature means adding an eighteenth prop.
Compound components solve this by moving structure out of props and into JSX composition. Instead of configuring behaviour through a flat list of props, you compose the component from named parts — each with its own props, its own responsibility, and its own place in the tree.
What compound components are
A compound component is a group of components that share implicit state — typically through context — and are designed to be used together.
The most familiar example you already use: HTML's <select> and <option>.
<option> only makes sense inside <select>. <select> needs <option> children to have anything to show. They share implicit state — which option is selected, which is highlighted — without you passing props between them. That coordination happens internally.
The React pattern mirrors this. A parent component provides state through context. Child components consume that context to render correctly. The consumer assembles the parts in whatever structure makes sense for their use case.
Building a Tabs component
A tabs component is the clearest demonstration. Here's the API we're building toward:
Clean. Readable. Adding a fourth tab means adding a Tabs.Tab and a Tabs.Panel — no new props on Tabs itself.
Step 1: The context
The shared state lives in context. Every child that needs to know the active tab or update it reads from here.
The null check in useTabsContext is important. It gives developers a clear error message when they use <Tabs.Tab> outside a <Tabs> — instead of a cryptic "cannot read properties of undefined."
Step 2: The parent component
The parent owns the state and provides it through context.
Step 3: The child components
Each child component consumes context and renders based on shared state.
Step 4: The static property pattern
The last piece — attaching child components to the parent as static properties. This is what enables the Tabs.List, Tabs.Tab, Tabs.Panel API:
TypeScript needs to know about these properties. The cleanest approach:
The complete working component
Here's the full Tabs implementation with a demo you can drop into any React project:
When compound components make sense
The pattern earns its complexity when:
The component has multiple named parts that users need to arrange. A Tabs has a list and panels. A Dialog has a trigger, content, header, and footer. A Accordion has items, each with a trigger and content. These are genuinely compositional.
Different consumers need different structures. One page might put the tab list at the top. Another might put it at the bottom. Compound components make both trivial. A prop-based API would need a tabListPosition prop and conditional rendering.
The parts share state that consumers shouldn't manage. The active tab, the open/closed state of a dialog, the selected item in a menu — this state is an implementation detail. Compound components keep it internal.
When to use something simpler
Single-purpose components don't benefit from the pattern. A Button, an Input, a Badge — these don't have parts. Adding the compound pattern here is complexity for its own sake.
Components with only two states. An Accordion with a single item that opens and closes is simpler as a controlled component with an open prop than as a compound component.
When the parts have no shared state. If the "parts" don't need to coordinate — if you're just grouping them visually — use a regular component with children or explicit slots via props.
The children prop alternative
Before reaching for compound components, consider whether explicit slot props cover the use case:
Slot props are simpler and often enough. Compound components earn their place when the slots need to coordinate — when what happens in one part affects another.
Up next: How to lazy-load components the right way — and avoid the trap — code splitting with React.lazy and Suspense without the waterfall.
Related: React context vs Zustand — when each one actually makes sense — the context patterns that power compound components and when to reach for something else.
Alex Chen is a senior frontend engineer who writes about React patterns, JavaScript internals, and the decisions that separate maintainable codebases from ones that fight back. Opinionated by design.
Photo by Tanja Tepavac on Unsplash

