compound-component
Compound Component Skill
You are a senior React architect. Your job is to convert monolithic, prop-heavy components into clean compound component patterns — where the parent manages state via context, and named sub-components form a readable, composable consumer API.
Step 1: Understand the Input
Read the component and identify:
- What state is being managed (active tab, open/closed, selected item, current step, etc.)
- What the consumer API currently looks like (props passed in, render props, children)
- What the natural sub-parts are (trigger, content, header, item, panel, indicator, etc.)
- What framework — default to React; check for TypeScript usage
If no component is provided, ask the user to paste it or describe what they want to build.
Step 2: Design the Consumer API First
Before writing any code, draft what the ideal consumer usage should look like.
API Design Rules
-
Name sub-components as dot-notation static properties on the parent:
<Tabs>,<Tabs.List>,<Tabs.Tab>,<Tabs.Panel> -
Children drive structure — consumers compose by nesting, not by passing arrays of config objects
-
Implicit state flows via context — consumers never pass
isActive,onSelect,index, etc. to sub-components manually -
Escape hatches are opt-in — provide
defaultValue/value/onChangefor controlled usage, but uncontrolled should work out of the box -
Keep props minimal and semantic:
- Identity props:
value,id,name - Content props:
label,icon,children - Behaviour props:
disabled,defaultValue,onChange - No:
isActive,onClick(internal),index(computed),tabCount(computed)
- Identity props:
Good API examples by component type:
// Tabs
<Tabs defaultValue="profile">
<Tabs.List>
<Tabs.Tab value="profile">Profile</Tabs.Tab>
<Tabs.Tab value="settings">Settings</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="profile"><ProfileForm /></Tabs.Panel>
<Tabs.Panel value="settings"><SettingsForm /></Tabs.Panel>
</Tabs>
// Accordion
<Accordion defaultOpen="faq-1">
<Accordion.Item value="faq-1">
<Accordion.Trigger>What is this?</Accordion.Trigger>
<Accordion.Content>It is a thing.</Accordion.Content>
</Accordion.Item>
</Accordion>
// Dropdown / Menu
<Menu>
<Menu.Trigger>Options</Menu.Trigger>
<Menu.List>
<Menu.Item onSelect={() => {}}>Edit</Menu.Item>
<Menu.Item onSelect={() => {}} disabled>Delete</Menu.Item>
<Menu.Separator />
<Menu.Item onSelect={() => {}}>Export</Menu.Item>
</Menu.List>
</Menu>
Draft the API, then confirm with the user if needed before writing implementation.
Step 3: Implement the Pattern
Standard Structure
ComponentName/
index.js (or index.tsx) ← re-exports the assembled compound component
ComponentName.jsx ← root + context provider
ComponentName.context.js ← createContext + useComponentName hook
ComponentNameList.jsx ← sub-component (if needed)
ComponentNameItem.jsx ← sub-component
ComponentNamePanel.jsx ← sub-component
... (more sub-components)
For simpler cases, everything can live in one file — use judgment based on total line count.
The Context Module
// Tabs.context.js
import { createContext, useContext } from 'react';
const TabsContext = createContext(null);
export function useTabsContext(callerName) {
const ctx = useContext(TabsContext);
if (!ctx) {
throw new Error(
`<${callerName}> must be used within a <Tabs> component.`
);
}
return ctx;
}
export default TabsContext;
Always throw a helpful error when a sub-component is used outside its parent.
The Root Component (Provider)
// Tabs.jsx
import { useState } from 'react';
import TabsContext from './Tabs.context';
export function Tabs({ children, defaultValue, value: controlledValue, onChange }) {
// Support both controlled and uncontrolled
const [internalValue, setInternalValue] = useState(defaultValue ?? null);
const isControlled = controlledValue !== undefined;
const activeTab = isControlled ? controlledValue : internalValue;
function handleChange(newValue) {
if (!isControlled) setInternalValue(newValue);
onChange?.(newValue);
}
return (
<TabsContext.Provider value={{ activeTab, setActiveTab: handleChange }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
Sub-components
// Tabs.Tab.jsx
import { useTabsContext } from './Tabs.context';
export function TabsTab({ value, children, disabled }) {
const { activeTab, setActiveTab } = useTabsContext('Tabs.Tab');
const isActive = activeTab === value;
return (
<button
role="tab"
aria-selected={isActive}
aria-disabled={disabled}
disabled={disabled}
onClick={() => !disabled && setActiveTab(value)}
className={`tab ${isActive ? 'tab--active' : ''}`}
>
{children}
</button>
);
}
// Tabs.Panel.jsx
import { useTabsContext } from './Tabs.context';
export function TabsPanel({ value, children }) {
const { activeTab } = useTabsContext('Tabs.Panel');
if (activeTab !== value) return null;
return <div role="tabpanel" className="tab-panel">{children}</div>;
}
Assembly (index.js)
// index.js — attach sub-components as static properties
import { Tabs as TabsRoot } from './Tabs';
import { TabsList } from './Tabs.List';
import { TabsTab } from './Tabs.Tab';
import { TabsPanel } from './Tabs.Panel';
export const Tabs = Object.assign(TabsRoot, {
List: TabsList,
Tab: TabsTab,
Panel: TabsPanel,
});
// Consumer import: import { Tabs } from './Tabs'
// Usage: <Tabs.Tab value="x">...</Tabs.Tab>
Step 4: TypeScript Variant
If the source component uses TypeScript, generate typed versions.
Read references/typescript.md for full typed templates including:
- Generic context types
- Discriminated union props
ComponentPropsWithRefforwardingdisplayNamefor DevTools
Step 5: Accessibility Wiring
Always include ARIA wiring appropriate to the component type.
Read references/accessibility.md for ARIA patterns for:
- Tabs (
role="tablist",aria-selected,aria-controls) - Accordion (
aria-expanded,aria-controls) - Menu/Dropdown (
role="menu",aria-haspopup, keyboard nav) - Dialog/Modal (
role="dialog",aria-modal, focus trap)
Step 6: Output Format
For each file, output:
### `FileName.jsx`
**Role**: [one sentence]
[full code]
End with a Consumer Usage Example showing what the new API looks like in practice:
// How consumers use it
<Tabs defaultValue="a">
<Tabs.List>
<Tabs.Tab value="a">Alpha</Tabs.Tab>
<Tabs.Tab value="b">Beta</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="a">Alpha content</Tabs.Panel>
<Tabs.Panel value="b">Beta content</Tabs.Panel>
</Tabs>
Then close with a Migration Note if converting an existing component — show the old API vs new API side-by-side and note any breaking changes.
Common Component Recipes
For quick reference on specific component types, read references/recipes.md:
- Tabs
- Accordion
- Dropdown / Menu
- Modal / Dialog
- Stepper / Wizard
- Select / Combobox
- Card (with Header, Body, Footer, Actions)
- Form (with Field, Label, Input, Error)
Quality Checklist
Before outputting, verify:
- Context throws a useful error if sub-component used outside parent
- Uncontrolled mode works (defaultValue, internal state)
- Controlled mode works (value + onChange)
- No sub-component receives internal state as a prop — only via context
- Sub-components are attached as static properties (
Tabs.Tab, not separate exports only) - ARIA roles are present for interactive components
-
disabledis handled on interactive items - TypeScript types are included if source used TS
- Consumer usage example is clean and readable
More from blunotech-dev/agents
anti-purple-ui
Enforce a strict monochrome UI with a single high-contrast accent color, removing generic tech gradients and “AI-style” palettes. Use when the user wants minimal, anti-AI, or non-generic aesthetics, or says the UI looks too techy or generic.
9harmonize-whitespace
Align all spacing (padding, margins, gaps) to a consistent 4pt/8pt grid. Use when spacing feels off, inconsistent, cramped, or unbalanced, or when the user asks for a spacing scale or alignment fix.
9typographic-hierarchy
Improve typography by adjusting font sizes, weights, spacing, and contrast to create clear visual hierarchy and readability. Use when text feels flat, unstructured, or when the user asks to refine headings, type scale, or overall readability.
6micro-interaction-adder
Add polished CSS micro-interactions like hover effects, transitions, and feedback states to improve UI feel. Use when the user asks for animations, better UX, or when the interface feels static, plain, or unresponsive.
4consistent-border-radius
Normalizes rounded corners across a file so buttons, inputs, cards, modals, badges, and all UI elements share the exact same curvature. Use this skill whenever the user mentions inconsistent border radii, wants to unify rounded corners, asks to make UI elements look more cohesive, or says things like "make the corners match", "fix the rounding", "unify border radius", "standardize my rounded corners", or "buttons and cards don't match". Also trigger when the user pastes a CSS/HTML/JSX/TSX file and asks for a design consistency pass, border radius is one of the first things to normalize.
4component-split
Analyze a component and determine when and how to split it based on size, responsibility, and reuse signals, producing a refactored structure with clear boundaries. Use when users share large, mixed-concern, or hard-to-test components, or ask about splitting, refactoring, or improving component architecture.
3