Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/rijvi-mahmud/shaddy/llms.txt

Use this file to discover all available pages before exploring further.

Native HTML gives you <select> and <option>, <table> with <tr> and <td>, <details> and <summary> — elements that work together without passing state through explicit props. The Compound Component pattern brings that same expressive, compositional API to your own React components, letting sub-components communicate through a shared React Context rather than a sprawling prop list.

The Problem

A component built with a flat prop API becomes unwieldy as requirements grow:
// ❌ Rigid props API — hard to customise individual items
<Dropdown
  trigger="Click me"
  items={[
    { label: 'Profile', onClick: handleProfile },
    { label: 'Settings', onClick: handleSettings },
    { label: 'Logout', onClick: handleLogout },
  ]}
  triggerClassName="btn-primary"
  itemClassName="menu-item"
/>
Adding icons, dividers, badges, or conditional items requires new props for every case. The API grows endlessly and still can’t cover every layout.

The Solution

Break the component into sub-components that share state through Context:
// ✅ Compound API — declarative and flexible
<Dropdown>
  <Dropdown.Trigger>Click me</Dropdown.Trigger>
  <Dropdown.Menu>
    <Dropdown.Item onClick={handleProfile}>Profile</Dropdown.Item>
    <Dropdown.Item onClick={handleSettings}>Settings</Dropdown.Item>
    <Dropdown.Divider />
    <Dropdown.Item onClick={handleLogout} variant="danger">Logout</Dropdown.Item>
  </Dropdown.Menu>
</Dropdown>
Callers control structure, content, and ordering. The parent component manages shared state behind the scenes via Context.

Building an Accordion

Here is a complete Accordion implementation demonstrating the pattern:
// components/Accordion.tsx
import { createContext, useContext, useState, ReactNode } from 'react'

// 1. Define the shared context type
interface AccordionContextType {
  openIndex: number | null
  setOpenIndex: (index: number | null) => void
}

const AccordionContext = createContext<AccordionContextType | null>(null)

// 2. Create a helper hook that throws a helpful error if used outside the parent
function useAccordionContext() {
  const context = useContext(AccordionContext)
  if (!context) {
    throw new Error('Accordion components must be used within <Accordion>')
  }
  return context
}

// 3. Root component — owns state and provides context
interface AccordionProps {
  children: ReactNode
  defaultIndex?: number | null
}

export function Accordion({ children, defaultIndex = null }: AccordionProps) {
  const [openIndex, setOpenIndex] = useState<number | null>(defaultIndex)

  return (
    <AccordionContext.Provider value={{ openIndex, setOpenIndex }}>
      <div className="border rounded-lg divide-y">{children}</div>
    </AccordionContext.Provider>
  )
}

// 4. Sub-components — consume context
function AccordionItem({ children, index }: { children: ReactNode; index: number }) {
  const { openIndex } = useAccordionContext()
  const isOpen = openIndex === index

  return <div className={isOpen ? 'bg-gray-50' : ''}>{children}</div>
}

function AccordionTrigger({ children, index }: { children: ReactNode; index: number }) {
  const { openIndex, setOpenIndex } = useAccordionContext()
  const isOpen = openIndex === index

  return (
    <button
      onClick={() => setOpenIndex(isOpen ? null : index)}
      className="w-full px-4 py-3 flex justify-between items-center text-left hover:bg-gray-100"
    >
      <span className="font-medium">{children}</span>
      <span className={`transform transition-transform ${isOpen ? 'rotate-180' : ''}`}></span>
    </button>
  )
}

function AccordionContent({ children, index }: { children: ReactNode; index: number }) {
  const { openIndex } = useAccordionContext()
  if (openIndex !== index) return null
  return <div className="px-4 py-3 text-gray-700">{children}</div>
}

// 5. Attach sub-components using dot notation
Accordion.Item = AccordionItem
Accordion.Trigger = AccordionTrigger
Accordion.Content = AccordionContent

Usage

import { Accordion } from './components/Accordion'

export function FAQSection() {
  return (
    <div className="max-w-2xl mx-auto p-6">
      <h2 className="text-2xl font-bold mb-4">Frequently Asked Questions</h2>

      <Accordion defaultIndex={0}>
        <Accordion.Item index={0}>
          <Accordion.Trigger index={0}>What is the Compound Component pattern?</Accordion.Trigger>
          <Accordion.Content index={0}>
            It allows components to share implicit state through React Context while exposing a
            declarative, composable JSX API.
          </Accordion.Content>
        </Accordion.Item>

        <Accordion.Item index={1}>
          <Accordion.Trigger index={1}>When should I use it?</Accordion.Trigger>
          <Accordion.Content index={1}>
            When building UI with multiple related parts that need to share state without prop
            drilling — tabs, accordions, modals, dropdowns.
          </Accordion.Content>
        </Accordion.Item>
      </Accordion>
    </div>
  )
}

Advanced Example: Tabs

// components/Tabs.tsx
import { createContext, useContext, useState, ReactNode } from 'react'

interface TabsContextType {
  activeTab: string
  setActiveTab: (id: string) => void
}

const TabsContext = createContext<TabsContextType | null>(null)

function useTabsContext() {
  const context = useContext(TabsContext)
  if (!context) throw new Error('Tabs components must be used within <Tabs>')
  return context
}

interface TabsProps {
  children: ReactNode
  defaultTab?: string
  onChange?: (tabId: string) => void
}

export function Tabs({ children, defaultTab, onChange }: TabsProps) {
  const [activeTab, setActiveTab] = useState<string>(defaultTab || '')

  const handleTabChange = (tabId: string) => {
    setActiveTab(tabId)
    onChange?.(tabId)
  }

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab: handleTabChange }}>
      <div className="w-full">{children}</div>
    </TabsContext.Provider>
  )
}

function TabList({ children, className = '' }: { children: ReactNode; className?: string }) {
  return <div className={`flex border-b border-gray-200 ${className}`}>{children}</div>
}

function Tab({ children, id, disabled = false }: { children: ReactNode; id: string; disabled?: boolean }) {
  const { activeTab, setActiveTab } = useTabsContext()
  const isActive = activeTab === id

  return (
    <button
      onClick={() => !disabled && setActiveTab(id)}
      disabled={disabled}
      className={`px-4 py-2 font-medium transition-colors
        ${isActive ? 'border-b-2 border-blue-500 text-blue-600' : 'text-gray-600 hover:text-gray-900'}
        ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
    >
      {children}
    </button>
  )
}

function TabPanels({ children }: { children: ReactNode }) {
  return <div className="mt-4">{children}</div>
}

function TabPanel({ children, id }: { children: ReactNode; id: string }) {
  const { activeTab } = useTabsContext()
  if (activeTab !== id) return null
  return <div>{children}</div>
}

Tabs.List = TabList
Tabs.Tab = Tab
Tabs.Panels = TabPanels
Tabs.Panel = TabPanel
// Dashboard.tsx
import { Tabs } from './components/Tabs'

export function Dashboard() {
  return (
    <Tabs defaultTab="overview" onChange={(tab) => console.log('Tab changed:', tab)}>
      <Tabs.List>
        <Tabs.Tab id="overview">Overview</Tabs.Tab>
        <Tabs.Tab id="users">Users</Tabs.Tab>
        <Tabs.Tab id="settings">Settings</Tabs.Tab>
      </Tabs.List>

      <Tabs.Panels>
        <Tabs.Panel id="overview">
          <div className="p-4 bg-gray-50 rounded-lg">Overview content goes here.</div>
        </Tabs.Panel>
        <Tabs.Panel id="users">
          <div className="p-4 bg-gray-50 rounded-lg">User management content goes here.</div>
        </Tabs.Panel>
        <Tabs.Panel id="settings">
          <div className="p-4 bg-gray-50 rounded-lg">Application settings go here.</div>
        </Tabs.Panel>
      </Tabs.Panels>
    </Tabs>
  )
}

When to Use

  • Building UI components with multiple related parts: tabs, accordions, dropdowns, modals.
  • You want callers to control layout and content without being constrained by props.
  • Multiple sub-components need shared state without prop drilling.
  • You are building a component library and want an intuitive, HTML-like API.

When Not to Use

  • The component is simple and a flat props API is perfectly readable.
  • Sub-components don’t share any state — Context adds unnecessary overhead.
  • Performance is critical and Context re-renders from state changes are a concern.
  • The added complexity of Context and sub-component registration doesn’t provide value.

Best Practices

  1. Use dot notation (Accordion.Item) so all sub-components are grouped under the parent and discoverable in your editor.
  2. Throw in context helpersuseAccordionContext should throw a clear error if called outside the parent, preventing silent misuse.
  3. Define TypeScript interfaces for the context shape and each sub-component’s props.
  4. Provide sensible defaults — components should work with minimal configuration.
  5. Keep sub-components focused — each one should have a single, clear responsibility.
Popular UI libraries such as Radix UI, Headless UI, and Chakra UI are built almost entirely around the Compound Component pattern. Studying their APIs is a great way to see the pattern applied at scale.

Build docs developers (and LLMs) love