Skip to content
Frontend
Skill

/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",

From plugin
tambo
11k8 skills2 agents4 commands

Claude Code

$ npx -y skills add tambo-ai/tambo --skill compound-components --agent claude-code

SKILL.md

compound-components.SKILL.md
name: building-compound-components
description: 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".
metadata:
  internal: true

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,
  Trigg
Read more
@tambo-ai
Ships withtambo

Generative UI SDK for React

11k8 skills2 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
11.2k
Star history of tambo-ai/tambo on GitHub, a chart by star-history.com

Other skills on tambo.

ai-sdk-model-manager

ai-sdk-model-manager

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

@tambo-ai@tambo-aiInstall Skill
api-resource-lifecycle

api-resource-lifecycle

Guides CRUD operations for API resources with cascading dependencies, descriptive validation, and orphan prevention. Use when…

@tambo-ai@tambo-aiInstall Skill
building-settings-ui

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…

@tambo-ai@tambo-aiInstall Skill
creating-styled-wrappers

creating-styled-wrappe…

Creates styled wrapper components that compose headless/base compound components. Use when refactoring styled components to use…

@tambo-ai@tambo-aiInstall Skill
validating-accessibility

validating-accessibili…

Use this skill when creating, modifying, or reviewing any .tsx component in apps/web, even if the user doesn't mention…

@tambo-ai@tambo-aiInstall Skill
building-with-tambo

building-with-tambo

Integrates Tambo into existing React apps — detects tech stack, installs @tambo-ai/react, wires TamboProvider, registers…

@tambo-ai@tambo-aiInstall Skill

More skills from other plugins.

next-bundle-optimizer
Auto-invoked

next-bundle-optimizer

Audit and reduce Next.js browser initial-load work. Use for slow route startup, oversized client bundles, duplicate browser…

nextjs1d143k
@vercel@vercelInstall Skill
migrate-radix-to-base

migrate-radix-to-base

Migrates React projects and components from Radix UI to Base UI. Use when asked to migrate from radix, move to base-ui, convert…

ui10d127k
@shadcn-ui@shadcn-uiInstall Skill
chrome-release-cls

chrome-release-cls

Given a Chrome Releases blog post URL (chromereleases.googleblog.com), extract every CVE/bug and find the underlying Gerrit CL…

electron1d124k
@electron@electronInstall Skill
taste-skill
Auto-invoked

taste-skill

tasteskill v2, the anti-slop frontend skill. Art direction for websites, distilled from a study of 247 curated gallery sites (One…

@leonxlnx@leonxlnxInstall Skill

Questions about compound-components.

Which coding agents can use compound-components?
compound-components installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.