Compound Components
Let a family of components share implicit state through context for a clean, expressive API.
A compound component is a set of components designed to work together as one unit — think <Tabs>, <Tab>, <TabPanel>, or <Select> and its <Option>s. The parent owns the state; the children read and update it implicitly through context, so the consumer writes clean markup without wiring props between siblings.
Why it feels so good to use
Compare passing an array of config objects versus writing real JSX:
<Tabs defaultValue="a">
<Tabs.List>
<Tabs.Tab value="a">Account</Tabs.Tab>
<Tabs.Tab value="b">Billing</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="a">Account settings</Tabs.Panel>
<Tabs.Panel value="b">Billing settings</Tabs.Panel>
</Tabs>The consumer controls structure, order, and what goes where — the component just coordinates. That flexibility is the whole reason design systems favor this pattern.
How it works
- The parent holds state and renders a Context Provider around
children. - Sub-components read that context to know the active value and to update it.
- Attach sub-components as properties (
Tabs.Tab = Tab) so they travel together and read naturally.
Example
import { createContext, useContext, useState } from 'react';
const TabsContext = createContext(null);
function useTabs() {
const ctx = useContext(TabsContext);
if (!ctx) throw new Error('Tabs.* must be used inside <Tabs>');
return ctx;
}
function Tabs({ defaultValue, children }) {
const [active, setActive] = useState(defaultValue);
return (
<TabsContext.Provider value={{ active, setActive }}>
<div>{children}</div>
</TabsContext.Provider>
);
}
function Tab({ value, children }) {
const { active, setActive } = useTabs();
return (
<button
onClick={() => setActive(value)}
aria-selected={active === value}
>
{children}
</button>
);
}
function Panel({ value, children }) {
const { active } = useTabs();
return active === value ? <div role="tabpanel">{children}</div> : null;
}
// Attach so they travel together and read as one API.
Tabs.Tab = Tab;
Tabs.Panel = Panel;
function Settings() {
return (
<Tabs defaultValue="account">
<Tabs.Tab value="account">Account</Tabs.Tab>
<Tabs.Tab value="billing">Billing</Tabs.Tab>
<Tabs.Panel value="account">Your account</Tabs.Panel>
<Tabs.Panel value="billing">Your billing</Tabs.Panel>
</Tabs>
);
}When to use it
- A <Tabs> component family where <Tabs.List>, <Tabs.Tab>, and <Tabs.Panel> share active-tab state through context without the consumer wiring any props between siblings.
- A design-system <Select> whose <Select.Option> children implicitly know the selected value and can mark themselves visually without receiving it as a prop.
- An <Accordion> where individual <Accordion.Item> panels communicate open/close state through a shared context rather than requiring an external state manager.
More examples
Tabs context and provider
The parent Tabs component owns the active-tab state and exposes it through a private context so any child in the tree can read or update it.
const TabsContext = React.createContext(null);
function Tabs({ defaultTab, children }) {
const [active, setActive] = React.useState(defaultTab);
return (
<TabsContext.Provider value={{ active, setActive }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}Tab and TabPanel sub-components
Tab and TabPanel each consume the shared context to react to the active tab without receiving any props from the consumer.
function Tab({ id, children }) {
const { active, setActive } = React.useContext(TabsContext);
return (
<button
aria-selected={active === id}
onClick={() => setActive(id)}
>
{children}
</button>
);
}
function TabPanel({ id, children }) {
const { active } = React.useContext(TabsContext);
return active === id ? <div role="tabpanel">{children}</div> : null;
}Static sub-component attachment
Attaching sub-components as static properties gives consumers a dot-notation API and makes the parent-child relationship visually explicit in JSX.
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
function App() {
return (
<Tabs defaultTab="home">
<Tabs.Tab id="home">Home</Tabs.Tab>
<Tabs.Tab id="settings">Settings</Tabs.Tab>
<Tabs.Panel id="home"><HomePage /></Tabs.Panel>
<Tabs.Panel id="settings"><SettingsPage /></Tabs.Panel>
</Tabs>
);
}
Discussion