ZyVOP Logo
Content That Connects
SeriesAI NewsCategoriesWrite for Us
ZyVOP Logo
Content That Connects

Empowering developers and creators with cutting-edge insights, comprehensive tutorials, and innovative solutions for the digital future.

Content

  • Tags
  • Badges
  • Write Article
  • Newsletter

Company

  • About Us
  • Write for Us
  • Contact

Connect

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA Policy
  • Code of Conduct

© 2026 ZyVOP. Crafted with care for the developer community.

Made with ❤️ by the ZyVOP team
All systems operational
HomeCompound Components: Designing an API, Not Just a Component

Compound Components: Designing an API, Not Just a Component

Maksym Kuzmitskyi (MaximusFT)
Maksym Kuzmitskyi (MaximusFT)Staff Frontend Engineer
July 31, 2026
6 min read
1 views
Compound Components: Designing an API, Not Just a Component
#react#Architecture#React Playbook

Compound Components: Designing an API, Not Just a Component

You build a <Select>. It starts clean — options in, value out. Then the requests arrive. Someone needs a divider between groups. Someone needs an icon next to certain options. Someone needs a sticky "create new" row at the bottom. Someone needs a disabled option with a tooltip explaining why. Each request adds a prop: groups, renderIcon, footerSlot, disabledReason. Six months later your <Select> takes twenty-three props, half of them undocumented, and every new requirement means editing the component and praying you didn't break the other twenty-two use-cases.

This is the prop-explosion death spiral, and it happens to every successful shared component. The problem isn't that you added props badly. It's that props were the wrong tool for the job somewhere around request number three. What you actually needed was to stop configuring a component and start composing one. That's what compound components are.

The last few articles have been about boundaries and where code lives. This one starts a run on the patterns you build inside those boundaries — the techniques that decide whether the components you write get reused or quietly rewritten. Compound components are the first one because they're the most common thing people get wrong.

The wall you hit with props

Props are perfect right up until the caller needs control over structure. A boolean, a string, a callback — props handle those beautifully. But the moment the caller wants to decide what goes where — the order of things, what sits between them, what wraps them — props turn into a configuration language you're now forced to invent and maintain.

// the prop-explosion version — every layout decision is a prop
<Select
  options={options}
  value={value}
  onChange={setValue}
  renderOption={(o) => <Icon name={o.icon} />}
  groupBy="category"
  groupHeaderRenderer={(g) => <strong>{g}</strong>}
  footer={<CreateNewRow />}
  showDividers
  dividerAfter={[2, 5]}
/>

Look at that dividerAfter={[2, 5]}. That's the smell. You're passing array indices to describe where a line should appear, because the component owns the structure and the caller has to negotiate with it through props. Every new layout wish is a new prop, a new branch inside the component, and a new way for the component to break. The component knows too much about every caller, and every caller knows too much about the component's internal prop vocabulary.

When callers start passing you layout instructions as props, they're asking for composition. Give it to them.

Compound components invert the control

A compound component flips the relationship. Instead of one component that takes a pile of props describing structure, you expose a set of components that the caller arranges themselves. The classic shape everyone knows is the native <select>:

<select>
  <option>One</option>
  <optgroup label="More">
    <option>Two</option>
  </optgroup>
</select>

You don't pass <select> a groups prop. You compose <option> and <optgroup> inside it. The React version of that idea looks like this:

<Select value={value} onChange={setValue}>
  <Select.Option value="one">One</Select.Option>
  <Select.Divider />
  <Select.Group label="More">
    <Select.Option value="two" icon={<StarIcon />}>Two</Select.Option>
  </Select.Group>
  <Select.Footer><CreateNewRow /></Select.Footer>
</Select>

Every one of those twenty-three props just became a component the caller places wherever they want. Need a divider after the second item? Put a <Select.Divider /> there. Need an icon? Pass it to that one <Select.Option>. The component stopped owning the structure, and the caller took it back — which is exactly what they were trying to do with all those props. You didn't add flexibility by adding configuration. You added it by removing configuration and letting composition do the work.

The part that makes it actually work: shared state

Here's the question that trips people up: if <Select.Option> is a separate component sitting who-knows-how-deep inside the tree, how does clicking it tell <Select> about the new value? You can't pass props down — you don't own the JSX in between. The answer is React Context, and this is the one piece of machinery that turns a pile of dotted components into a real compound component.

const SelectContext = createContext<{
  value: string;
  onChange: (v: string) => void;
} | null>(null);

function Select({ value, onChange, children }: SelectProps) {
  return (
    <SelectContext.Provider value={{ value, onChange }}>
      <div role="listbox">{children}</div>
    </SelectContext.Provider>
  );
}

function Option({ value, icon, children }: OptionProps) {
  const ctx = useContext(SelectContext);
  if (!ctx) throw new Error('<Select.Option> must be used inside <Select>');
  const selected = ctx.value === value;
  return (
    <div
      role="option"
      aria-selected={selected}
      onClick={() => ctx.onChange(value)}
      className={selected ? 'bg-[#22c55e]/10' : ''}
    >
      {icon}
      {children}
    </div>
  );
}

// attach the children as properties — this is what enables the dot syntax
Select.Option = Option;
Select.Divider = () => <div role="separator" className="my-1 border-t border-gray-200" />;
Select.Group = Group;
Select.Footer = Footer;

The parent puts the shared state into context. Every child reads from context, so it doesn't matter how deeply it's nested or what the caller wraps it in — an <Option> three divs down still knows the current value and can report a click. That context is the invisible wire connecting components the caller arranged. Props flow explicitly to configure one option (its icon, its value); context flows implicitly to coordinate all of them. That division is the whole trick.

Notice the throw in Option. Because these components only make sense together, a compound component should fail loudly when used apart. A bare <Select.Option> with no <Select> around it is a bug, and catching it with a clear error is far kinder than letting it silently render nothing.

When it earns its place — and when it doesn't

Compound components are not free. They're more code, more indirection, and a context that has to exist. So I don't reach for them by default. I reach for them when three signals show up together.

The caller needs to control structure. If people keep asking to reorder parts, inject things between parts, or wrap parts — that's the tell. Structure control is what props are bad at and composition is good at.

The parts are meaningless alone but meaningful together. Tabs/TabList/Tab/TabPanel. Accordion/AccordionItem. Menu/MenuItem/MenuSeparator. None of those children make sense on their own, and all of them need to coordinate through shared state. That's the exact shape compound components fit.

There's real shared state to coordinate. If the children don't need to know about each other or a common value, you don't need the context, and you probably don't need this pattern — you just need components that accept children.

And when not to: a component with two or three stable props and no structural variation is perfect as a plain component. Wrapping a <Button> in a compound API is pure ceremony. The pattern earns its complexity only when the alternative is prop explosion. If you're not drowning in configuration props, you don't have the problem this solves.

The mental shift that matters

The reason I wanted to open the patterns run with this one is that compound components force a change in how you think, and that change outlasts the specific technique. The first time your component grows past a handful of props, you're not building a component anymore. You're designing an API that other people — including future you — will program against. And APIs are judged by how gracefully they handle the requests you didn't anticipate.

A prop-based component handles the unanticipated request by growing a new prop, and eventually collapses under its own configuration surface. A compound component handles it by letting the caller compose a solution you never had to predict — the divider you didn't plan for, the footer you didn't imagine — without you touching the component at all. That's the difference between a component people configure and a component people build with. The first one you maintain forever. The second one mostly maintains itself.

Next I want to push this one step further, to the pattern that takes the same idea and removes the last assumption a compound component still makes — that you own the markup at all. Headless components, render props, hooks-as-API: handing over behavior while giving up the UI entirely. If you've got a shared component in your codebase with more than a dozen props, go count them, then ask how many are really the caller trying to control structure. I'd bet the number is higher than you'd like — tell me what you find.

Maksym Kuzmitskyi (MaximusFT)

Maksym Kuzmitskyi (MaximusFT)

Staff Frontend Engineer

Passionate developer sharing knowledge about modern web technologies and best practices.

Comments (0)

Login to post a comment.

Related Posts

How to Make a Product Demo Video in React

This is a build log for a 30-second product demo video, written entirely in React and rendered to an .mp4 you can drop on a landing page, post to X, or attach t...

Read article

Service Discovery with Eureka and Spring Cloud: A Production Hands-On Guide

Learn how to build production-grade service discovery with Eureka and Spring Cloud LoadBalancer. Includes real-world heartbeat tuning and memory optimization tips.

Read article

Monorepo Best Practices

I learned to love monorepos the hard way — by suffering through the alternative. At a large Canadian automotive company, we had a repository for basic UI compon...

Read article

React Performance Optimization

I joined a large Canadian automotive company as one of three senior frontend engineers. The site was public-facing — a product catalog where users browsed vehic...

Read article

Complex Forms Done Right

At Motorola Solutions I built a multi-step contract wizard where users selected products, services, additional options, and pricing tiers — dozens of fields tha...

Read article