Compound Components
The compound components pattern creates a group of components that work together and share implicit state. It gives the consumer flexible composition control.
Classic example - Select / Tabs
tsx
// usage - clean, expressive API
<Tabs defaultValue="profile">
<Tabs.List>
<Tabs.Trigger value="profile">Profile</Tabs.Trigger>
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
</Tabs.List>
<Tabs.Panel value="profile"><ProfileForm /></Tabs.Panel>
<Tabs.Panel value="settings"><SettingsForm /></Tabs.Panel>
</Tabs>Implementation with Context
tsx
interface TabsContext { active: string; setActive: (v: string) => void; }
const TabsCtx = createContext<TabsContext | null>(null);
const useTabs = () => useContext(TabsCtx)!;
function Tabs({ defaultValue, children }: { defaultValue: string; children: React.ReactNode }) {
const [active, setActive] = useState(defaultValue);
return (
<TabsCtx.Provider value={{ active, setActive }}>
<div>{children}</div>
</TabsCtx.Provider>
);
}
function TabsList({ children }: { children: React.ReactNode }) {
return <div role="tablist">{children}</div>;
}
function TabsTrigger({ value, children }: { value: string; children: React.ReactNode }) {
const { active, setActive } = useTabs();
return (
<button
role="tab"
aria-selected={active === value}
onClick={() => setActive(value)}
>
{children}
</button>
);
}
function TabsPanel({ value, children }: { value: string; children: React.ReactNode }) {
const { active } = useTabs();
return active === value ? <div role="tabpanel">{children}</div> : null;
}
// attach as static properties
Tabs.List = TabsList;
Tabs.Trigger = TabsTrigger;
Tabs.Panel = TabsPanel;Why it's better than prop drilling
tsx
// without compound components - prop soup
<Tabs
items={['Profile', 'Settings']}
panels={[<ProfileForm />, <SettingsForm />]}
defaultIndex={0}
onTabClick={...}
/>This pattern is used by Radix UI, Headless UI, Ark UI and shadcn/ui. Learning it is essential for building flexible design system components.
updated
did you know it?