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
- A parent component owns the shared state and wraps its children in a context provider.
- Child components consume that context to read or update the shared state — no props need to pass between siblings.
- 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
| Pros | Cons |
|---|---|
| Flexible composition — consumers rearrange children freely | Requires context boilerplate (create, provider, hook) |
| Clean API surface — one import, dot-notation access | Child components only work inside their parent provider; standalone use breaks |
| Implicit state sharing — no prop drilling between siblings | Dot-notation assignment is not tree-shakeable |