Compound components are a React pattern that lets one widget expose several small parts that share hidden state while the consumer arranges them freely. Instead of a single component with twenty props, you get a tree like Menu, Menu.Button, and Menu.List that reads like the UI it builds.
The problem it solves
A monolithic component locks the structure down. If you want to insert a divider between two menu items, or move the button after the list, the component needs yet another prop, and the API grows without limit.
The compound pattern inverts that. The root owns the state, the parts read it, and the consumer owns the layout. This is the API shape behind libraries like Radix UI.
Share state through context
The root component holds the open state and exposes it through a context provider. Nothing else sees the state directly.
import { createContext, useContext, useState } from "react";
const MenuContext = createContext(null);
export function Menu({ children }) {
const [open, setOpen] = useState(false);
return (
<MenuContext.Provider value={{ open, setOpen }}>
{children}
</MenuContext.Provider>
);
}The provider wraps whatever children the consumer passes, so the state is available to every part without prop drilling. The children can be anything, which is what makes the arrangement flexible.
Read the context with a hook
Each part reads the context through a custom hook. The hook throws when used outside the provider, which turns a silent bug into a clear message.
function useMenu() {
const context = useContext(MenuContext);
if (!context) {
throw new Error("Menu parts must render inside <Menu>");
}
return context;
}The throw is deliberate. If someone renders Menu.Button without a Menu ancestor, the mistake is caught immediately instead of failing later with an undefined state.
Build the parts
The button and the list are tiny components that read only what they need. The button toggles, and the list renders only when open.
function Button({ children }) {
const { open, setOpen } = useMenu();
return (
<button aria-expanded={open} onClick={() => setOpen(!open)}>
{children}
</button>
);
}
function List({ children }) {
const { open } = useMenu();
if (!open) return null;
return <ul role="menu">{children}</ul>;
}Each part is self contained, and neither knows how the other is styled or ordered. The item completes the set by closing the menu after an action.
function Item({ children, onSelect }) {
const { setOpen } = useMenu();
return (
<li>
<button role="menuitem" onClick={() => { onSelect(); setOpen(false); }}>
{children}
</button>
</li>
);
}Attach the parts to the root
The final step names the pieces as static properties on Menu, which groups them into one discoverable API.
Menu.Button = Button;
Menu.List = List;
Menu.Item = Item;
<Menu>
<Menu.Button>Actions</Menu.Button>
<Menu.List>
<Menu.Item onSelect={() => console.log("edit")}>Edit</Menu.Item>
<Menu.Item onSelect={() => console.log("delete")}>Delete</Menu.Item>
</Menu.List>
</Menu>The consumer now controls the tree, and the shared open state flows invisibly between the parts. A full menu built this way is in the dropdown menu guide.
When the pattern pays off
Use compound components when several parts must share state but the consumer needs to control their order and markup. Menus, tabs, accordions, and form fields are the classic cases.
Skip it when the widget is simple enough to be one component with a few props, or when the parts never rearrange. The extra context layer is overhead until the flexibility is actually used.
The case for letting a library own the semantics instead of rebuilding them is in headless UI components.
Rune AI
Key Insights
- Keep shared state in the root component and expose it with context.
- Add a custom hook that throws when used outside the provider.
- Attach each part to the root with a static property.
- Let the consumer control order, markup, and extra elements.
- Use the pattern when parts must share state but stay rearrangeable.
Frequently Asked Questions
What problem do compound components solve?
How do the parts share state?
Is this the same as context alone?
Conclusion
Compound components share one piece of state through context while exposing small building blocks the consumer arranges freely. Build a provider at the root, a hook to read it, and attach each part to the root component as a static property.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.