Compound Components
Split a complex UI into a parent and named child components (e.g. Card.Header, Card.Body) so the API is flexible and the structure is clear in JSX without a huge prop list.
The problem I keep seeing
Components like Card, Accordion, or Tabs have multiple slots (title, body, footer, panels) and optional parts. Exposing everything as props leads to a long, brittle API and doesn’t make the structure obvious in the tree. You want a pattern where the consumer composes visible pieces (e.g. Card.Header, Card.Body) and the parent shares context with those pieces.
Naive approach
One component with many props: title, subtitle, footer, variant, etc. Usage is a single tag with a long prop list; order and presence of sections are implicit.
First improvement
Compound components: a parent (e.g. Card) that provides context and wraps children, and subcomponents (e.g. Card.Header, Card.Body) attached to the parent. The consumer writes clear JSX with only the sections they need; the parent and children share state via context.
Remaining issues
- Child validation: You may want to allow only certain subcomponents; you can
React.Children.mapand check type or displayName, or document the contract and rely on usage. - Flexible order: Sometimes order matters (e.g. Tab list before Tab panels); compound components make that order explicit in JSX.
- TypeScript: Typing the compound (parent + attached subcomponents) is a bit verbose but doable with an interface that has both the component and nested component types.
Production pattern
Define the parent component and attach subcomponents as static properties (e.g. Card.Title = CardTitle). Use React context in the parent to pass variant, size, or open state to the subcomponents. Subcomponents consume context and render their slice of the UI. No need to pass every option through the parent’s props; the structure is visible in the tree. For stricter contracts, validate children in the parent or use a small helper that only renders known types.
When I use this
- Cards, panels, accordions: Multiple named regions with shared styling/state; consumer picks which regions to use.
- Tabs, dropdowns: List of triggers + content panels that need to share “active” state.
- Skip when: The component has one or two slots; a few props are simpler than compound components.
Gotchas
- Context scope: Only descendants of the parent get the context; so
Card.Titlemust be rendered insideCard. - Naming: Attach subcomponents so the API is
Card.Titlenot a separateCardTitleimport; keeps the relationship clear. - Render props vs compounds: For “custom render per item” (e.g. each tab panel can be a function), combine with render props or pass a function as child; for fixed structure, compound components are enough.