Verified against Claude · 2026-07-29
Build a compound component that lets the consumer control markup, not just props
Implements a Tabs- or Accordion-style compound component using a narrowly-scoped context so subcomponents share implicit state while the consumer keeps full control over composition and order.
The prompt
Ready to copy — highlighted parts are example details you can swap.
You are building a compound component — a family of subcomponents that share implicit state through context, so the consumer composes the markup directly while the state and behavior stay coordinated behind the scenes. COMPONENT FAMILY Tabs SUBCOMPONENTS Tabs.List, Tabs.Tab, Tabs.Panels, Tabs.Panel SHARED STATE Which tab is currently active, and whether the active tab was set by click versus keyboard arrow navigation. CONSUMER CUSTOMIZATION NEEDS Must be able to render tabs in any order, wrap an individual Tab in a custom badge component, and render panels lazily so an inactive panel is not mounted until first selected. IMPLEMENTATION RULES Create one context scoped exclusively to this component family — never reuse or extend an app-wide context for this — and provide it from the root component of the family, never from an individual subcomponent. Each subcomponent reads only the specific piece of the shared context it actually needs; do not have every subcomponent destructure the entire context value if only one field is relevant to it, since that couples subcomponents to internal shape changes they don't actually depend on. The consumer must be able to reorder, omit, or wrap individual subcomponents in their own markup without breaking the shared state — if a subcomponent's position or presence is hardcoded into the root component's own render logic instead of left to the consumer's JSX, that defeats the actual purpose of a compound component and should be flagged, not implemented that way. Throw a clear, descriptive error — not a silent undefined — if a subcomponent is rendered outside its required root component, so a consumer mistake fails loudly during development rather than producing a confusing runtime bug. Keep the context value stable across renders where possible — memoize the object passed to the provider so subcomponents that don't care about a particular state change don't re-render needlessly, especially since this is exactly the context re-render behavior that makes an unscoped, unmemoized context expensive at scale. Do not let this pattern quietly grow into project-wide global state — if a future feature needs data from outside this component family, that is a sign this state has outgrown the compound-component pattern and needs a different home, not a widened context. If the family needs to expose an imperative action to the consumer — programmatically opening a specific tab, or scrolling a specific accordion panel into view — prefer a controlled prop pattern (an activeTab plus onActiveTabChange pair the consumer can own) over reaching for useImperativeHandle and a ref, since a controlled prop keeps the family's state consistent with whatever the consumer's own state management expects, while an imperative ref-based API creates a second, parallel way to change the same state that can drift out of sync with the first. OUTPUT FORMAT 1. The root component and its context provider, as real code. 2. Each subcomponent, showing exactly what slice of context it reads. 3. A usage example showing the consumer's actual JSX, demonstrating the customization named above. 4. The explicit error thrown for misuse, with its message text.
Customize
Optional — swap in your own details for the highlighted parts above.
Why this works
Scoping the context strictly to this one component family, and providing it only from the family's own root, is what actually delivers the pattern's core promise — a consumer who can freely reorder or wrap subcomponents in their own JSX without touching a single prop, because the coordination lives in context rather than in an explicit parent-managed prop chain that would otherwise force a fixed rendering order. The rule against each subcomponent destructuring the entire context value is a direct, practical application of the same re-render mechanic relevant to any React context: even inside a small, well-scoped compound component, a subcomponent that reads the whole context object re-renders on every state change in that context regardless of relevance, and at a large enough tab or accordion count that adds up to a measurable cost for a pattern specifically chosen for its ergonomics, not its performance ceiling. Throwing a loud, descriptive error when a subcomponent renders outside its root targets the single most common consumer mistake with any compound component — rendering Tabs.Tab somewhere it isn't wrapped in Tabs — and the difference between a clear thrown error naming the exact problem and a silent undefined deep in a context read is the difference between a five-second fix and a confusing debugging session for whoever hits it, often not the original author. Preferring a controlled prop pair over an imperative ref-based API for external control matters for the same reason controlled inputs are generally preferred over uncontrolled ones with escape-hatch refs: a consumer who can set activeTab directly and read updates through onActiveTabChange has exactly one source of truth to reason about, whereas a ref exposing an imperative openTab(id) method creates a second write path into the same state that the family's own internal state can silently disagree with the moment the two are driven from different places — a bug that specifically tends to surface only when a consumer starts combining the imperative method with the family's own default uncontrolled behavior in the same integration.
What you get back
const TabsContext = createContext(null); function Tabs({ children, defaultTab }) { const [activeTab, setActiveTab] = useState(defaultTab); const value = useMemo(() => ({ activeTab, setActiveTab }), [activeTab]); return <TabsContext.Provider value={value}>{children}</TabsContext.Provider>; } function useTabsContext() { const ctx = useContext(TabsContext); if (!ctx) throw new Error('Tabs.Tab and Tabs.Panel must be rendered inside a <Tabs> component.'); return ctx; } function Tab({ id, children }) { const { activeTab, setActiveTab } = useTabsContext(); return <button role="tab" aria-selected={activeTab === id} onClick={() => setActiveTab(id)}>{children}</button>; } // Consumer usage — full control over order and wrapping: <Tabs defaultTab="billing"> <Tabs.List> <Tabs.Tab id="billing">Billing</Tabs.Tab> <Badge count={2}><Tabs.Tab id="alerts">Alerts</Tabs.Tab></Badge> </Tabs.List> </Tabs>
Verified against
Claude Sonnet 4.6 · 2026-07-29
Claude Code Sonnet 4.6 · 2026-08-02
Changelog
- 2026-07-29 — Initial publish, verified against Claude and Claude Code on Sonnet 4.6.
Need this built into your business?
If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.
EXPLORE CUSTOM SOFTWARE
