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
- Use dot notation (
Accordion.Item) so all sub-components are grouped under the parent and discoverable in your editor.
- Throw in context helpers —
useAccordionContext should throw a clear error if called outside the parent, preventing silent misuse.
- Define TypeScript interfaces for the context shape and each sub-component’s props.
- Provide sensible defaults — components should work with minimal configuration.
- 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.