building-compound-components

2
1
Source

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

Install

mkdir -p .claude/skills/building-compound-components && curl -L -o skill.zip "https://mcp.directory/api/skills/download/7572" && unzip -o skill.zip -d .claude/skills/building-compound-components && rm skill.zip

Installs to .claude/skills/building-compound-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, 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

ScenarioPatternWhy
Static contentDirect childrenSimplest, most flexible
Need internal stateRender propExplicit state access
List/iterationSub-contextEach item gets own context
Element polymorphismasChildChange underlying element
CSS-only stylingData attributesNo 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

components

tambo-ai

Creates and registers Tambo components - generative (AI creates on-demand) and interactable (pre-placed, AI updates). Use when defining components, working with TamboComponent, withInteractable, propsSchema, or registering components for AI to render or update.

66

component-rendering

tambo-ai

Handles Tambo component streaming, loading states, and persistent state. Use when implementing streaming UI feedback, tracking prop streaming status, managing partial props, or persisting component state across sessions with useTamboStreamStatus or useTamboComponentState.

92

add-to-existing-project

tambo-ai

Integrates Tambo into EXISTING React projects by detecting the tech stack and adapting installation. Use when adding Tambo to an existing app, integrating with current frameworks, or when the user has an existing codebase they want to add AI/generative UI to. For starting a NEW project from scratch, use start-from-scratch skill instead. For registering existing components, use add-components-to-registry skill.

42

tools-and-context

tambo-ai

Provides Tambo with data and capabilities via custom tools, MCP servers, context helpers, and resources. Use when registering tools Tambo can call, connecting MCP servers, adding context to messages, implementing @mentions, or providing additional data sources with defineTool, mcpServers, contextHelpers, or useTamboContextAttachment.

01

add-components-to-registry

tambo-ai

Registers existing React components with Tambo so AI can render them. Use when user wants to make their existing components available to AI, register components for generative UI, convert React components to Tambo components, or mentions /add-components-to-registry. For creating NEW components, see the components skill. For project setup, use add-to-existing-project or start-from-scratch skills.

01

cli

tambo-ai

Tambo CLI reference for project setup and component installation. Agent-friendly with non-interactive mode and exit codes. Use when running tambo init, tambo add, npx tambo commands, or browsing the component library. For guided project creation with tech recommendations, use start-from-scratch skill. For adding Tambo to existing projects, use add-to-existing-project skill.

21

You might also like

ui-ux-pro-max

nextlevelbuilder

"UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 8 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient."

1,5561,556

flutter-development

aj-geddes

Build beautiful cross-platform mobile apps with Flutter and Dart. Covers widgets, state management with Provider/BLoC, navigation, API integration, and material design.

1,8251,484

drawio-diagrams-enhanced

jgtolentino

Create professional draw.io (diagrams.net) diagrams in XML format (.drawio files) with integrated PMP/PMBOK methodologies, extensive visual asset libraries, and industry-standard professional templates. Use this skill when users ask to create flowcharts, swimlane diagrams, cross-functional flowcharts, org charts, network diagrams, UML diagrams, BPMN, project management diagrams (WBS, Gantt, PERT, RACI), risk matrices, stakeholder maps, or any other visual diagram in draw.io format. This skill includes access to custom shape libraries for icons, clipart, and professional symbols.

1,7051,235

godot

bfollington

This skill should be used when working on Godot Engine projects. It provides specialized knowledge of Godot's file formats (.gd, .tscn, .tres), architecture patterns (component-based, signal-driven, resource-based), common pitfalls, validation tools, code templates, and CLI workflows. The `godot` command is available for running the game, validating scripts, importing resources, and exporting builds. Use this skill for tasks involving Godot game development, debugging scene/resource files, implementing game systems, or creating new Godot components.

1,609902

pdf-to-markdown

aliceisjustplaying

Convert entire PDF documents to clean, structured Markdown for full context loading. Use this skill when the user wants to extract ALL text from a PDF into context (not grep/search), when discussing or analyzing PDF content in full, when the user mentions "load the whole PDF", "bring the PDF into context", "read the entire PDF", or when partial extraction/grepping would miss important context. This is the preferred method for PDF text extraction over page-by-page or grep approaches.

1,886835

nano-banana-pro

garg-aayush

Generate and edit images using Google's Nano Banana Pro (Gemini 3 Pro Image) API. Use when the user asks to generate, create, edit, modify, change, alter, or update images. Also use when user references an existing image file and asks to modify it in any way (e.g., "modify this image", "change the background", "replace X with Y"). Supports both text-to-image generation and image-to-image editing with configurable resolution (1K default, 2K, or 4K for high resolution). DO NOT read the image file first - use this skill directly with the --input-image parameter.

1,435791