In React, what is a compound component API such as `<Tabs><TabList><Tab/></TabList><TabPanel/></Tabs>`, and what do you gain and lose compared with a single `<Tabs items={[...]} />` that takes a config array?
answer
- two ways to expose one widget
- config prop grows a prop per layout
- parent keeps state, consumer keeps markup
- state travels by context, not props
- flexibility bought with discoverability
basics
~20 sA compound component splits one widget into a parent and related parts that share state implicitly through React context, so the consumer arranges the parts in JSX. It trades discoverability and enforceable structure for markup and layout freedom.
solid answer
~40 sWith a config-array API the parent owns both the state and the markup, so every new layout need becomes another prop — `renderLabel`, `tabClassName`, `separatorAfter`. A compound API inverts that: `Tabs` owns only the state, publishes it through context, and the consumer composes `TabList`, `Tab` and `TabPanel` themselves, wrapping or reordering them freely without any new prop. The cost is that the coupling is invisible: the type of `Tabs` cannot say which children are legal, a `Tab` rendered outside its `Tabs` fails only at runtime, and the API is only as learnable as its examples. I reach for config props for a constrained internal widget, and for a compound API in a design-system component that has to survive many different layouts.
go deeper
Be able to recognise the shape — a parent plus related parts used together in JSX — and say that the parts get shared state from the parent rather than from props the consumer passes.
Explain that the parent holds the state and publishes it through context, and contrast that with a config prop where every layout variation costs another prop on the parent.
Show the judgment: name the discoverability and support costs of an implicit API, and say which kind of component in a real codebase earns one.
Own the library-wide policy — which widgets get a compound API, what that makes permanent public surface, and how you keep visual consistency when consumers arrange the parts themselves.
## What "compound component" names A compound component is an API shape, not a React feature. One widget is published as several components meant to be used together — `Tabs`, `TabList`, `Tab`, `TabPanel` — where the outer one owns the state and the inner ones read it without the consumer wiring anything up: ```jsx <Tabs defaultValue="overview"> <TabList> <Tab id="overview">Overview</Tab> <Tab id="billing">Billing</Tab> </TabList> <TabPanel id="overview">…</TabPanel> <TabPanel id="billing">…</TabPanel> </Tabs> ``` Nothing here passes the selected tab to `Tab`. `Tabs` keeps it in state and publishes it on a context that the parts read. HTML has the same idea in `<select>` and `<option>`: the option does not carry the select's value, but the pair is only meaningful together. ## The API it competes with The alternative is one component that takes the whole widget as data: ```jsx <Tabs defaultValue="overview" items={[ { id: 'overview', label: 'Overview', content: <Overview /> }, { id: 'billing', label: 'Billing', content: <Billing /> }, ]} /> ``` This is a genuinely good API and often the right one. It is discoverable — autocomplete on the props tells you everything the component can do. It is checkable — the type of `items` describes exactly what is legal. And it is structurally impossible to misuse: there is no way to forget a panel or nest the parts wrongly, because the consumer never touches the structure. Its failure mode is prop growth. Someone wants a count badge on one tab, so you add `renderLabel`. Someone wants a divider between groups, so you add `separatorAfter`. Someone wants the tab strip sticky, so you add `listClassName`. Someone wants a search box inside the strip and there is no prop for that at all. Each addition exists only so the consumer can influence markup the component insists on owning, and after a dozen of them the component has re-implemented JSX in prop form, badly. ## What the compound shape buys **Markup freedom.** The consumer writes the tree, so wrapping, ordering, extra elements and custom styling need no cooperation from the library. The search box is just another child of `TabList`. **Extension without an API change.** A team can put their own component between the parts, or render `Tab` inside their own `Tooltip`, and the library never learns about it. **State still in one place.** Flexibility here is only about structure. `Tabs` remains the single owner of which tab is selected, so the parts cannot disagree. **Correctness the library can still own.** The parent can generate ids with `useId` and hand each part the ids it needs, so the `aria-controls`/`aria-labelledby` wiring between a tab and its panel is done by the library rather than by whoever composed the markup. ## What it costs **The contract is invisible.** The parent's props type says `children: ReactNode`, which permits literally anything. Nothing stops a consumer from omitting the panels, rendering two `TabList`s, or dropping a `Tab` from a different widget in the middle. This is not a gap you can close by trying harder with types: the pattern's whole value is that parts may sit at any depth inside arbitrary wrappers, and that is exactly what defeats a static description of the allowed children. **Discoverability drops.** With a config prop, the props panel is the documentation. With a compound API, the reader has to know that `TabPanel` exists and that it belongs inside `Tabs`. Examples become the real specification, and an undocumented part effectively does not exist. **Misuse surfaces late.** A part used outside its parent finds no provider and gets the context's default value, so the failure appears as a confusing render or a crash somewhere else unless you deliberately make it throw a named error. **More surface to learn and to keep.** Four exports instead of one, each of them public API from the moment you ship it. **Consistency erodes.** If forty product surfaces compose the parts themselves, you will get forty slightly different tab strips, which is the opposite of what a design system is for. ## How to choose Ask whether the arrangement is a real variable in your product. If consumers keep asking for one more prop to influence layout, the arrangement varies and the compound API pays for itself. If the widget only makes sense in one shape — a confirm dialog, a labelled field with its error message — the flexibility buys nothing and puts correctness in the consumer's hands. The two are not mutually exclusive, and the mature answer usually ships both: build the compound parts as the primitive layer, then export an opinionated preset composed from those same parts for the common case. Consumers reach for the preset by default and drop to the parts only when they genuinely need a different structure.
- Can you keep a compound API and still guarantee the accessibility wiring between a tab and its panel?Yes, as long as the parent owns it. `Tabs` generates ids with `useId` and hands each part its own id plus its partner's through context, so `aria-controls` and `aria-labelledby` are produced by the library, not by whoever composed the markup. What you still cannot force is that a panel exists for every tab — that needs a development-time warning rather than a type.
- When does the flexibility of a compound API actually backfire in a product codebase?When the arrangement was never really a variable. If every team composes the parts slightly differently, you get inconsistent UI plus a support queue about layouts you never intended, and the library has no way to fix them centrally. That is the signal to ship an opinionated preset built from the same parts and make it the documented default path.
saying these in an interview costs you the question
- Claims TypeScript can restrict which children the parent accepts
- Thinks React passes the parent's state down automatically
- Calls it a performance optimization rather than an API choice
- Assumes the parts must be direct children of the parent
- Says a config-prop API is simply the worse option