const TabsContext = createContext(null);
function Tabs({ defaultId, children }) {
const [activeId, setActiveId] = useState(defaultId);
return <TabsContext value={{ activeId, setActiveId }}>{children}</TabsContext>;
}
function Tab({ id, children }) {
const { activeId, setActiveId } = useContext(TabsContext);
return <button aria-selected={activeId === id} onClick={() => setActiveId(id)}>{children}</button>;
}
Compound components are a set of components that work together and share hidden state. An example is <Tabs>, <Tabs.List>, <Tabs.Tab> and <Tabs.Panel>. The user arranges them freely in JSX, while the parent manages the shared state. Think of <select> and <option> in HTML: they only make sense together.
The modern implementation uses context:
- The parent
TabsholdsactiveIdin state. - It provides
{ activeId, setActiveId }through a context. - Each
Tabreads the context to know if it is selected, and to change the selection. - Each
Panelrenders only when its id matches.
Also expose a custom hook that throws a clear error when a piece is used outside the parent.
This gives a flexible, readable API without long lists of configuration props. Users can place and style the pieces as they like. Many headless UI libraries are built this way. Headless means they provide behaviour but no styles.
- Compound components are related components that share hidden state.
- The parent owns the state and provides it through context.
- The children read the context to render and to update the state.
- A guard hook throws a clear error when a piece is used outside the parent.
- Users get flexible markup instead of many configuration props.