building-compound-components
Enables creation of headless, compound components using a pattern similar to Radix UI.
Install
mkdir -p .claude/skills/building-compound-components && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/7572" && unzip -o skill.zip -d .claude/skills/building-compound-components && rm skill.zipInstalls to .claude/skills/building-compound-components
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Creates unstyled compound components that separate business logic from styles. Use when building headless UI primitives, creating component libraries, implementing Radix-style namespaced components, or when the user mentions "compound components", "headless", "unstyled", "primitives", or "render props".Key capabilities
- →Isolate component business logic from style layers
- →Implement Radix-style namespaced components
- →Expose component behavior through context providers
- →Enforce render-prop patterns for consumer access
How it works
Follows a strict architecture of separating hooks into private internal files while exposing public components through context providers.
Inputs & outputs
When to use building-compound-components
- →Creating reusable component libraries
- →Building headless UI primitives
- →Implementing namespaced component structures
- →Structuring complex UI components
About this skill
Building Compound Components
Create unstyled, composable React components following the Radix UI / Base UI pattern. Components expose behavior via context while consumers control rendering.
Project Rules
These rules are specific to this codebase and override general patterns.
Hooks Are Internal
Hooks are implementation details, not public API. Never export hooks from the index.
// index.tsx - CORRECT
export const Component = {
Root: ComponentRoot,
Content: ComponentContent,
};
export type { ComponentRootProps, ComponentContentRenderProps };
// index.tsx - WRONG
export { useComponentContext }; // Don't export hooks
Consumers access state via render props, not hooks. When styled wrappers in the same package need hook access, import directly from the source file:
import { useComponentContext } from "../base/component/component-context";
No Custom Data Fetching in Primitives
Base components can use @tambo-ai/react SDK hooks (components require Tambo provider anyway). Custom data fetching logic (combining sources, external providers) belongs in the styled layer.
// OK - SDK hooks in primitive
const Root = ({ children }) => {
const { value, setValue, submit } = useTamboThreadInput();
const { isIdle, cancel } = useTamboThread();
return <Context.Provider value={{ value, setValue, isIdle }}>{children}</Context.Provider>;
};
// WRONG - custom data fetching in primitive
const Textarea = ({ resourceProvider }) => {
const { data: mcpResources } = useTamboMcpResourceList(search);
const externalResources = useFetchExternal(resourceProvider);
const combined = [...mcpResources, ...externalResources];
return <div>{combined.map(...)}</div>;
};
Pre-computed Props Arrays for Collections
When exposing collections via render props, pre-compute all props in a memoized array rather than providing a getter function.
// AVOID - getter function pattern
const Items = ({ children }) => {
const { rawItems, selectedId, removeItem } = useContext();
const getItemProps = (index: number) => ({
/* new object every call */
});
return children({ items: rawItems, getItemProps });
};
// PREFERRED - pre-computed array
const Items = ({ children }) => {
const { rawItems, selectedId, removeItem } = useContext();
const items = React.useMemo<ItemRenderProps[]>(
() =>
rawItems.map((item, index) => ({
item,
index,
isSelected: selectedId === item.id,
onSelect: () => setSelectedId(item.id),
onRemove: () => removeItem(item.id),
})),
[rawItems, selectedId, removeItem],
);
return children({ items });
};
Workflow
Copy this checklist and track progress:
Compound Component Progress:
- [ ] Step 1: Create context file
- [ ] Step 2: Create Root component
- [ ] Step 3: Create consumer components
- [ ] Step 4: Create namespace export (index.tsx)
- [ ] Step 5: Verify all guidelines met
Step 1: Create context file
my-component/
├── index.tsx
├── component-context.tsx
├── component-root.tsx
├── component-item.tsx
└── component-content.tsx
Create a context with a null default and a hook that throws on missing provider:
// component-context.tsx
const ComponentContext = React.createContext<ComponentContextValue | null>(
null,
);
export function useComponentContext() {
const context = React.useContext(ComponentContext);
if (!context) {
throw new Error("Component parts must be used within Component.Root");
}
return context;
}
export { ComponentContext };
Step 2: Create Root component
Root manages state and provides context. Use forwardRef, support asChild via Radix Slot, and expose state via data attributes:
// component-root.tsx
export const ComponentRoot = React.forwardRef<
HTMLDivElement,
ComponentRootProps
>(({ asChild, defaultOpen = false, children, ...props }, ref) => {
const [isOpen, setIsOpen] = React.useState(defaultOpen);
const Comp = asChild ? Slot : "div";
return (
<ComponentContext.Provider
value={{ isOpen, toggle: () => setIsOpen(!isOpen) }}
>
<Comp ref={ref} data-state={isOpen ? "open" : "closed"} {...props}>
{children}
</Comp>
</ComponentContext.Provider>
);
});
ComponentRoot.displayName = "Component.Root";
Step 3: Create consumer components
Choose the composition pattern based on need:
Direct children (simplest, for static content):
const Content = ({ children, className, ...props }) => {
const { data } = useComponentContext();
return (
<div className={className} {...props}>
{children}
</div>
);
};
Render prop (when consumer needs internal state):
const Content = ({ children, ...props }) => {
const { data, isLoading } = useComponentContext();
const content =
typeof children === "function" ? children({ data, isLoading }) : children;
return <div {...props}>{content}</div>;
};
Sub-context (for lists where each item needs own context):
const Steps = ({ children }) => {
const { reasoning } = useMessageContext();
return (
<StepsContext.Provider value={{ steps: reasoning }}>
{children}
</StepsContext.Provider>
);
};
const Step = ({ children, index }) => {
const { steps } = useStepsContext();
return (
<StepContext.Provider value={{ step: steps[index], index }}>
{children}
</StepContext.Provider>
);
};
Step 4: Create namespace export
// index.tsx
export const Component = {
Root: ComponentRoot,
Trigger: ComponentTrigger,
Content: ComponentContent,
};
// Re-export types only - never hooks
export type { ComponentRootProps } from "./component-root";
export type { ComponentContentProps } from "./component-content";
Step 5: Verify guidelines
- No styles in primitives - consumers control all styling via className/props
- Data attributes for CSS - expose state like
data-state="open",data-disabled,data-loading - Support asChild - let consumers swap the underlying element via Radix
Slot - Forward refs - always use
forwardRef - Display names - set for DevTools (
Component.Root,Component.Item) - Throw on missing context - fail fast with clear error messages
- Export types - consumers need
ComponentProps,RenderPropsinterfaces - Hooks stay internal - never export from index, expose state via render props
- SDK hooks OK, custom fetching not -
@tambo-ai/reacthooks are fine, combining logic goes in styled layer - Pre-compute collection props - use
useMemoarrays, not getter functions
Pattern Selection
| Scenario | Pattern | Why |
|---|---|---|
| Static content | Direct children | Simplest, most flexible |
| Need internal state | Render prop | Explicit state access |
| List/iteration | Sub-context | Each item gets own context |
| Element polymorphism | asChild | Change underlying element |
| CSS-only styling | Data attributes | No JS needed for style variants |
Anti-Patterns
- Hardcoded styles - primitives should be unstyled
- Prop drilling - use context instead
- Missing error boundaries - throw when context is missing
- Inline functions in render prop types - define proper interfaces
- Default exports - use named exports in namespace object
- Exporting hooks - hooks are internal; expose state via render props
- Custom data fetching in primitives - SDK hooks are fine, but combining/external fetching belongs in styled layer
- Re-implementing base logic - styled wrappers should compose, not duplicate
- Getter functions for collections - pre-compute props arrays in useMemo instead
When not to use it
- →For simple, standalone UI elements without sub-parts
- →When creating purely styled wrappers with no internal logic
Limitations
- →Hooks are restricted to internal package use
- →Requires adherence to Tambo-ai architectural rules
How it compares
It mandates a headless, composable architecture rather than standard monolithic component patterns.
Compared to similar skills
building-compound-components side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| building-compound-components (this skill) | 1 | 4mo | No flags | Intermediate |
| react-modernization | 21 | 2mo | No flags | Advanced |
| state-management | 1 | 5mo | No flags | Beginner |
| writing-react-effects | 1 | 3mo | No flags | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by tambo-ai
View all by tambo-ai →You might also like
react-modernization
wshobson
Upgrade React applications to latest versions, migrate from class components to hooks, and adopt concurrent features. Use when modernizing React codebases, migrating to React Hooks, or upgrading to latest React versions.
state-management
redpanda-data
Manage client and server state with Zustand stores and React Query patterns.
writing-react-effects
dust-tt
Writes React components without unnecessary useEffect. Use when creating/reviewing React components, refactoring effects, or when code uses useEffect to transform data or handle events.
form-refactorer
redpanda-data
Refactor legacy forms to use modern Redpanda UI Registry Field components with react-hook-form and Zod validation. Use when user requests: (1) Form refactoring or modernization, (2) Converting Chakra UI or @redpanda-data/ui forms, (3) Updating forms to use Field components, (4) Migrating from legacy form patterns, (5) Implementing forms with react-hook-form and Zod validation.
rendering-conditional-render
TheOrcDev
Use explicit ternary operators instead of && for conditional rendering. Apply when rendering values that could be 0, NaN, or other falsy values that might render unexpectedly.
creating-styled-wrappers
tambo-ai
Creates styled wrapper components that compose headless/base compound components. Use when refactoring styled components to use base primitives, implementing opinionated design systems on top of headless components, or when the user mentions "use base components", "compose primitives", "styled wrapper", or "refactor to use base".