Last updated: Apr 29, 2026

Compound Components

Compound components are a group of components that work together to form a single UI element, sharing implicit state through a context provider. The idea mirrors native HTML — <select> and <option> are compound components: <select> manages the selected value, and each <option> registers itself without you wiring anything manually. In React, the same idea is expressed by attaching child components as static properties on a parent (Accordion.Item, Accordion.Panel), so consumers import one namespace and compose pieces freely via dot notation.

How It Works

  1. A parent component owns the shared state and wraps its children in a context provider.
  2. Child components consume that context to read or update the shared state — no props need to pass between siblings.
  3. Children are attached as static properties on the parent (Accordion.Item = Item), giving consumers a single import with dot-notation access to every piece.

Example

import {
  createContext,
  useContext,
  useState,
  type ReactNode,
} from "react";

interface AccordionContext {
  activeIndex: number | null;
  toggle: (index: number) => void;
}

const AccordionCtx = createContext<AccordionContext | null>(null);

function useAccordion(): AccordionContext {
  const ctx = useContext(AccordionCtx);
  if (!ctx) throw new Error("Accordion.* must be used within <Accordion>");
  return ctx;
}

function Accordion({ children }: { children: ReactNode }) {
  const [activeIndex, setActiveIndex] = useState<number | null>(null);
  const toggle = (index: number) =>
    setActiveIndex((prev) => (prev === index ? null : index));

  return (
    <AccordionCtx.Provider value={{ activeIndex, toggle }}>
      {children}
    </AccordionCtx.Provider>
  );
}

function Item({
  index,
  children,
}: {
  index: number;
  children: ReactNode;
}) {
  const { activeIndex, toggle } = useAccordion();
  return (
    <div>
      <button onClick={() => toggle(index)}>
        {activeIndex === index ? "▼" : "▶"} Toggle
      </button>
      {activeIndex === index && <div>{children}</div>}
    </div>
  );
}

Accordion.Item = Item;

Usage looks like this:

function App() {
  return (
    <Accordion>
      <Accordion.Item index={0}>First panel content</Accordion.Item>
      <Accordion.Item index={1}>Second panel content</Accordion.Item>
      <Accordion.Item index={2}>Third panel content</Accordion.Item>
    </Accordion>
  );
}

Each Accordion.Item reads the shared active index from context and toggles itself on click. The consumer never passes state between siblings — Accordion handles it all internally.

Trade-offs

ProsCons
Flexible composition — consumers rearrange children freelyRequires context boilerplate (create, provider, hook)
Clean API surface — one import, dot-notation accessChild components only work inside their parent provider; standalone use breaks
Implicit state sharing — no prop drilling between siblingsDot-notation assignment is not tree-shakeable