DEV Community

Cover image for Compound Component Pattern — Flexible APIs for Cohesive UI
Md Enayetur Rahman
Md Enayetur Rahman

Posted on

Compound Component Pattern — Flexible APIs for Cohesive UI

Insights & examples inspired by *“Learning Patterns” by Lydia Hallie & Addy Osmani***

“When multiple elements belong together and need to share implicit state, treat them as compound components so consumers compose, not configure.”


1  What is the Compound Component Pattern?

It’s a design pattern where several sub‑components collaborate under a single parent that holds the state and logic. Each child is responsible only for rendering its slice of UI, but they communicate through the parent’s React Context (or cloneElement). This yields APIs that feel like native HTML combos such as <select> + <option> or <details> + <summary>.

<Toggle>
  <Toggle.On>Light is ON</Toggle.On>
  <Toggle.Off>Light is OFF</Toggle.Off>
  <Toggle.Button />
</Toggle>
Enter fullscreen mode Exit fullscreen mode

The consumer decides ordering and layout; the parent Toggle silently wires up state.


2  Why use it?

Benefit How it helps
Implicit wiring Consumers don’t pass callbacks or state props—less prop‑drilling, fewer mistakes.
Flexible composition UI authors reorder, omit, or duplicate parts as needed.
Reusability Sub‑components are tiny & focused; you can reuse them elsewhere.
Accessibility wins The parent guarantees correct ARIA roles/labels, even if children shuffle.

Patterns.dev emphasises that compound components shine when parts depend on each other yet need independent markup freedom.


3  Implementing a simple Toggle

3.1 Create shared context

const ToggleContext = React.createContext();
Enter fullscreen mode Exit fullscreen mode

3.2 Parent with state

function Toggle({ children }) {
  const [on, setOn] = React.useState(false);
  const toggle = () => setOn(!on);

  const value = React.useMemo(() => ({ on, toggle }), [on]);
  return (
    <ToggleContext.Provider value={value}>
      {children}
    </ToggleContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

3.3 Children consume context

Toggle.On = function On({ children }) {
  const { on } = React.useContext(ToggleContext);
  return on ? children : null;
};

Toggle.Off = function Off({ children }) {
  const { on } = React.useContext(ToggleContext);
  return on ? null : children;
};

Toggle.Button = function Button(props) {
  const { on, toggle } = React.useContext(ToggleContext);
  return (
    <button onClick={toggle} aria-pressed={on} {...props}>
      {on ? "Switch off" : "Switch on"}
    </button>
  );
};
Enter fullscreen mode Exit fullscreen mode

Using React Context keeps the API “clean and intuitive,” mirroring native controls.


4  Five Real‑Life Software Use Cases

  1. Radix UI<Tabs.List>, <Tabs.Trigger>, <Tabs.Content> all inherit the parent Tabs context, letting developers rearrange tab parts freely.
  2. React Router<Routes> manages location state; nested <Route> elements render depending on it, no manual prop passing.
  3. Headless UI / Menu<Menu>, <Menu.Button>, <Menu.Items>, <Menu.Item> cooperate for accessible dropdowns.
  4. Material‑UI Form<FormGroup> provides context to <FormControlLabel> and <Checkbox> to sync value and validation.
  5. Downshift Toggle / Select – Kent C. Dodds’ library exposes compound children like <Downshift.Item> to build custom autocompletes with minimal glue.

5  When not to use it

If children don’t depend on shared state, or if you need strict structure (e.g., fixed HTML), simpler patterns like Container/Presentational may suffice. Overusing compound components can hide data‑flow and complicate debugging. Balance is key.


6  Key take‑aways

  • Design for composition, not configuration.
  • Parent holds logic + accessibility; children hold markup.
  • Ideal for dropdowns, tabs, menus, forms, toggles—any clustered UI with inter‑dependent pieces.

Content sources: patterns.dev (Learning Patterns) and related community articles (citations above). Licensed under CC BY‑NC 4.0 where applicable.

Top comments (0)