Agent skill

building-compound-components

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".

Stars 11,120
Forks 560

Install this agent skill to your Project

npx add-skill https://github.com/tambo-ai/tambo/tree/main/.claude/skills/compound-components

Metadata

Additional technical details for this skill

internal
YES

SKILL.md

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.

tsx
// 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:

tsx
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.

tsx
// 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.

tsx
// 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:

tsx
// 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:

tsx
// 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):

tsx
const Content = ({ children, className, ...props }) => {
  const { data } = useComponentContext();
  return (
    <div className={className} {...props}>
      {children}
    </div>
  );
};

Render prop (when consumer needs internal state):

tsx
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):

tsx
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

tsx
// 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, RenderProps interfaces
  • Hooks stay internal - never export from index, expose state via render props
  • SDK hooks OK, custom fetching not - @tambo-ai/react hooks are fine, combining logic goes in styled layer
  • Pre-compute collection props - use useMemo arrays, 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

Expand your agent's capabilities with these related and highly-rated skills.

tambo-ai/tambo

creating-styled-wrappers

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".

11,120 560
Explore
tambo-ai/tambo

api-resource-lifecycle

Guides CRUD operations for API resources with cascading dependencies, descriptive validation, and orphan prevention. Use when adding delete/remove operations, creating validation logic, building resources that depend on other resources, or when the user mentions "cascade delete", "orphan records", "duplicate detection", "validation errors", "resource cleanup", or "rollback on failure".

11,120 560
Explore
tambo-ai/tambo

validating-accessibility

Use this skill when creating, modifying, or reviewing any .tsx component in apps/web, even if the user doesn't mention "accessibility." Covers semantic HTML, aria labels, navigation landmarks, forms, dialogs, and keyboard navigation. Trigger on: adding buttons, links, toggles, icons, or any interactive element; building or editing forms; adding dialogs or modals; reviewing UI code. Includes inline verification patterns for scanning violations. Not for styling or layout changes that don't involve interactive elements.

11,120 560
Explore
tambo-ai/tambo

building-settings-ui

Use this skill when adding or modifying settings UI in Tambo Cloud. Covers where a new settings section belongs (Agent tab vs Settings tab), and the component patterns used across both pages (card layout, toasts, confirmation dialogs, destructive styling, save behavior conventions). Triggers on "add a new settings section", "where should X go?", "settings UI", "settings page", "agent page", or any work touching apps/web/components/dashboard-components/project-details/, project-settings.tsx, or agent-settings.tsx. Not for full-stack feature building (DB, tRPC, tests); those patterns will get their own skills.

11,120 560
Explore
tambo-ai/tambo

ai-sdk-model-manager

Manages AI SDK model configurations - updates packages, identifies missing models, adds new models with research, and updates documentation

11,120 560
Explore
tambo-ai/tambo

generative-ui

Creates a new Tambo generative UI app from scratch. Scaffolds with tambo create-app, wires TamboProvider, registers starter components. Triggers on "new Tambo app", "create a generative UI app", "build an AI app from scratch", "start a new project with Tambo". For existing apps, use building-with-tambo.

11,120 560
Explore

Didn't find tool you were looking for?

Be as detailed as possible for better results