import React, { createContext, useContext, useState, ReactNode } from 'react'
interface TabsContextType {
activeTab: string
setActiveTab: (tab: string) => void
}
const TabsContext = createContext<TabsContextType | undefined>(undefined)
function useTabsContext() {
const context = useContext(TabsContext)
if (!context) {
throw new Error('Tabs components must be used within Tabs')
}
return context
}
interface TabsProps {
defaultValue: string
children: ReactNode
}
function TabsRoot({ defaultValue, children }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultValue)
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
)
}
interface TabsListProps {
children: ReactNode
}
function TabsList({ children }: TabsListProps) {
return <div className="flex border-b">{children}</div>
}
interface TabsTriggerProps {
value: string
children: ReactNode
}
function TabsTrigger({ value, children }: TabsTriggerProps) {
const { activeTab, setActiveTab } = useTabsContext()
const isActive = activeTab === value
return (
<button
onClick={() => setActiveTab(value)}
className={`px-4 py-2 font-medium transition-colors ${
isActive
? 'border-b-2 border-blue-600 text-blue-600'
: 'text-gray-600 hover:text-gray-900'
}`}
>
{children}
</button>
)
}
interface TabsContentProps {
value: string
children: ReactNode
}
function TabsContent({ value, children }: TabsContentProps) {
const { activeTab } = useTabsContext()
if (activeTab !== value) return null
return <div className="py-4">{children}</div>
}
export const Tabs = Object.assign(TabsRoot, {
List: TabsList,
Trigger: TabsTrigger,
Content: TabsContent,
})
function PostDetail() {
return (
<Tabs defaultValue="details">
<Tabs.List>
<Tabs.Trigger value="details">Details</Tabs.Trigger>
<Tabs.Trigger value="comments">Comments</Tabs.Trigger>
<Tabs.Trigger value="history">History</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="details">
<PostDetails />
</Tabs.Content>
<Tabs.Content value="comments">
<CommentsList />
</Tabs.Content>
<Tabs.Content value="history">
<PostHistory />
</Tabs.Content>
</Tabs>
)
}
Compound components create flexible, composable APIs by sharing state between parent and child components via context. Instead of passing dozens of props, child components access shared state through context. A Select component might have Select.Trigger, Select.Options, and Select.Option subcomponents that work together. This pattern provides flexibility—consumers can rearrange, style, or extend components—while maintaining encapsulation. I use TypeScript's namespace syntax or dot notation to attach subcomponents. The parent component provides context, and children consume it with useContext. This is the pattern used by libraries like Radix UI and Headless UI for building accessible, customizable component primitives.