Skip to content

React Patterns: The Compound Component Pattern

Posted on:January 12, 2026 at 10:00 AM

Hey there, Fellow Dev! 👋

Have you ever tried to build a reusable component, like a generic Tabs or Accordion, and found yourself passing a million props just to configure it?

You end up with something scary like this:

// The "God Component" approach
<Tabs
  data={[
    { label: "Home", content: "Welcome home!" },
    { label: "Profile", content: "User profile here." }
  ]}
  theme="dark"
  activeTabIndex={0}
  onTabChange={handleTabChange}
  tabClassName="text-bold"
  panelClassName="p-4"
/>

It works, but it’s rigid. What if you want to add an icon to just one tab? Or disable the second tab? You’d have to add more properties to that data array. It gets messy fast. 🤯

Enter the Compound Component Pattern.

What is it?

Think about standard HTML tags like <select> and <option>. They don’t work alone; they work together.

<select>
  <option value="1">Option 1</option>
  <option value="2">Option 2</option>
</select>

The <select> manages the state, and the <option>s render the choices. They share state implicitly. We can do the same in React!

Step 1: Create the Context

First, we need a way for the parent (Tabs) to talk to its children (Tab and TabPanel) without passing props manually. We use React Context for this.

import React, { createContext, useContext, useState } from 'react';

// 1. Create the Context
const TabsContext = createContext();

// 2. Create the Parent Component
const Tabs = ({ children, defaultIndex = 0 }) => {
  const [activeIndex, setActiveIndex] = useState(defaultIndex);

  return (
    <TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
      <div className="tabs-container">
        {children}
      </div>
    </TabsContext.Provider>
  );
};

Here, Tabs holds the state (activeIndex) and provides it to anyone inside it.

Step 2: Create the Sub-Components

Now, let’s create the components that will consume this context.

// 3. Create the Tab List (optional wrapper for styling)
const TabList = ({ children }) => {
  return <div className="flex border-b">{children}</div>;
};

// 4. Create the individual Tab
const Tab = ({ index, children }) => {
  const { activeIndex, setActiveIndex } = useContext(TabsContext);
  const isActive = activeIndex === index;

  return (
    <button
      onClick={() => setActiveIndex(index)}
      className={`p-2 ${isActive ? 'border-b-2 border-blue-500 font-bold' : 'text-gray-500'}`}
    >
      {children}
    </button>
  );
};

// 5. Create the Panel
const TabPanel = ({ index, children }) => {
  const { activeIndex } = useContext(TabsContext);

  if (activeIndex !== index) return null;

  return <div className="p-4">{children}</div>;
};

Step 3: Use It!

Now look how clean and flexible our API is:

const App = () => {
  return (
    <Tabs defaultIndex={0}>
      <TabList>
        <Tab index={0}>🏠 Home</Tab>
        <Tab index={1}>👤 Profile</Tab>
        <Tab index={2}>⚙️ Settings</Tab>
      </TabList>

      <TabPanel index={0}>
        <h2>Welcome to the Dashboard</h2>
        <p>This uses the Compound Component pattern!</p>
      </TabPanel>
      <TabPanel index={1}>
        <UserProfile />
      </TabPanel>
      <TabPanel index={2}>
        <p>Settings go here...</p>
      </TabPanel>
    </Tabs>
  );
};

Why is this better?

  1. Flexibility: I can easily swap the order of tabs, wrap them in divs for styling, or add icons without changing the Tabs logic.
  2. Readability: It looks like HTML. You can see the structure immediately.
  3. Separation of Concerns: Tabs handles the logic (state), while Tab and TabPanel handle the rendering.

When to use it?

Use this pattern when you have a group of components that need to share state and logic implicitly, like:

Closing

The Compound Component pattern is a powerful tool in your React arsenal. It shifts the complexity from the consumer (you) to the library author (also you, but the future you).

Start using it for your reusable UI components, and your future self will thank you!

Happy Coding! 🚀