Compound Components in React: Build Flexible UI APIs

Build flexible React component APIs with the compound components pattern. Share state through context and let users arrange the parts.

6 min read

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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
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

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.
RunePowered by Rune AI

Frequently Asked Questions

What problem do compound components solve?

They let the consumer rearrange and interleave the parts of a widget while the parts still share one hidden piece of state. A monolithic component with many props cannot offer that flexibility.

How do the parts share state?

The root component holds the state and passes it down through a context provider. Each part reads the context through a custom hook, so no prop drilling is needed.

Is this the same as context alone?

Context is the mechanism, and compound components are the API built on top of it. The pattern packages the provider and consumers into one discoverable component tree.

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.