creating-styled-wrappers

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Styling Compound Wrappers

样式化复合包装组件

Create styled wrapper components that compose headless base compound components. This skill complements
building-compound-components
(which builds the base primitives) by focusing on how to properly consume and wrap them with styling and additional behavior.
Real-world example: See references/real-world-example.md for a complete before/after MessageInput refactoring.
创建可组合无头基础复合组件的样式化包装组件。该技能是对
building-compound-components
(用于构建基础原语)的补充,重点关注如何通过样式和附加行为正确使用和包装这些基础组件
真实场景示例:查看references/real-world-example.md获取MessageInput组件重构的完整前后对比。

Core Principle: Compose, Don't Duplicate

核心原则:组合,而非重复

Styled wrappers should compose base components, not re-implement their logic.
tsx
// WRONG - re-implementing what base already does
const StyledInput = ({ children, className }) => {
  const { value, setValue, submit } = useTamboThreadInput(); // Duplicated!
  const [isDragging, setIsDragging] = useState(false); // Duplicated!
  const handleDrop = useCallback(/* ... */); // Duplicated!

  return (
    <form onDrop={handleDrop} className={className}>
      {children}
    </form>
  );
};

// CORRECT - compose the base component
const StyledInput = ({ children, className, variant }) => {
  return (
    <BaseInput.Root className={cn(inputVariants({ variant }), className)}>
      <BaseInput.Content className="rounded-xl data-[dragging]:border-dashed">
        {children}
      </BaseInput.Content>
    </BaseInput.Root>
  );
};
样式化包装组件应组合基础组件,而非重复实现其逻辑。
tsx
// WRONG - re-implementing what base already does
const StyledInput = ({ children, className }) => {
  const { value, setValue, submit } = useTamboThreadInput(); // Duplicated!
  const [isDragging, setIsDragging] = useState(false); // Duplicated!
  const handleDrop = useCallback(/* ... */); // Duplicated!

  return (
    <form onDrop={handleDrop} className={className}>
      {children}
    </form>
  );
};

// CORRECT - compose the base component
const StyledInput = ({ children, className, variant }) => {
  return (
    <BaseInput.Root className={cn(inputVariants({ variant }), className)}>
      <BaseInput.Content className="rounded-xl data-[dragging]:border-dashed">
        {children}
      </BaseInput.Content>
    </BaseInput.Root>
  );
};

Refactoring Workflow

重构工作流程

Copy this checklist and track progress:
Styled Wrapper Refactoring:
- [ ] Step 1: Identify duplicated logic
- [ ] Step 2: Import base components
- [ ] Step 3: Wrap with Base Root
- [ ] Step 4: Apply state-based styling and behavior
- [ ] Step 5: Wrap sub-components with styling
- [ ] Step 6: Final verification
复制以下检查清单并跟踪进度:
Styled Wrapper Refactoring:
- [ ] Step 1: Identify duplicated logic
- [ ] Step 2: Import base components
- [ ] Step 3: Wrap with Base Root
- [ ] Step 4: Apply state-based styling and behavior
- [ ] Step 5: Wrap sub-components with styling
- [ ] Step 6: Final verification

Step 1: Identify Duplicated Logic

步骤1:识别重复逻辑

Look for patterns that indicate logic should come from base:
  • SDK hooks (
    useTamboThread
    ,
    useTamboThreadInput
    , etc.)
  • Context creation (
    React.createContext
    )
  • State management that mirrors base component state
  • Event handlers (drag, submit, etc.) that base components handle
寻找表明逻辑应来自基础组件的模式:
  • SDK钩子(
    useTamboThread
    useTamboThreadInput
    等)
  • Context创建(
    React.createContext
  • 与基础组件状态镜像的状态管理
  • 基础组件已处理的事件处理器(拖拽、提交等)

Step 2: Import Base Components

步骤2:导入基础组件

tsx
import { MessageInput as MessageInputBase } from "@tambo-ai/react-ui-base/message-input";
tsx
import { MessageInput as MessageInputBase } from "@tambo-ai/react-ui-base/message-input";

Step 3: Wrap with Base Root

步骤3:用基础Root组件包装

Replace custom context/state management with the base Root:
tsx
// Before
const MessageInput = ({ children, variant }) => {
  return (
    <MessageInputInternal variant={variant}>{children}</MessageInputInternal>
  );
};

// After
const MessageInput = ({ children, variant, className }) => {
  return (
    <MessageInputBase.Root className={cn(variants({ variant }), className)}>
      {children}
    </MessageInputBase.Root>
  );
};
替换自定义Context/状态管理为基础Root组件:
tsx
// Before
const MessageInput = ({ children, variant }) => {
  return (
    <MessageInputInternal variant={variant}>{children}</MessageInputInternal>
  );
};

// After
const MessageInput = ({ children, variant, className }) => {
  return (
    <MessageInputBase.Root className={cn(variants({ variant }), className)}>
      {children}
    </MessageInputBase.Root>
  );
};

Step 4: Apply State-Based Styling and Behavior

步骤4:应用基于状态的样式和行为

State access follows a hierarchy — use the simplest option that works:
  1. Data attributes (preferred for styling) — base components expose
    data-*
    attributes
  2. Render props (for behavior changes) — use when rendering different components
  3. Context hooks (for sub-components) — OK for styled sub-components needing deep context access
tsx
// BEST - data-* classes for styling, render props only for behavior
// Note: use `data-[dragging]:*` syntax (v3-compatible), not `data-dragging:*` (v4 only)
const StyledContent = ({ children }) => (
  <BaseComponent.Content
    className={cn(
      "group rounded-xl border",
      "data-[dragging]:border-dashed data-[dragging]:border-emerald-400",
    )}
  >
    {({ elicitation, resolveElicitation }) => (
      <>
        {/* Drop overlay uses group-data-* for styling */}
        <div className="hidden group-data-[dragging]:flex absolute inset-0 bg-emerald-50/90">
          <p>Drop files here</p>
        </div>

        {elicitation ? (
          <ElicitationUI
            request={elicitation}
            onResponse={resolveElicitation}
          />
        ) : (
          children
        )}
      </>
    )}
  </BaseComponent.Content>
);

// OK - styled sub-components can use context hook for deep access
const StyledTextarea = ({ placeholder }) => {
  const { value, setValue, handleSubmit, editorRef } = useMessageInputContext();
  return (
    <CustomEditor
      ref={editorRef}
      value={value}
      onChange={setValue}
      onSubmit={handleSubmit}
      placeholder={placeholder}
    />
  );
};
When to use context hooks vs render props:
  • Render props: when the parent wrapper needs state for behavior changes
  • Context hooks: when a styled sub-component needs values not exposed via render props
状态访问遵循层级结构——使用最简单可行的方案:
  1. 数据属性(样式首选)——基础组件会暴露
    data-*
    属性
  2. 渲染属性(用于行为变更)——在渲染不同组件时使用
  3. Context钩子(用于子组件)——适用于需要深度Context访问的样式化子组件
tsx
// BEST - data-* classes for styling, render props only for behavior
// Note: use `data-[dragging]:*` syntax (v3-compatible), not `data-dragging:*` (v4 only)
const StyledContent = ({ children }) => (
  <BaseComponent.Content
    className={cn(
      "group rounded-xl border",
      "data-[dragging]:border-dashed data-[dragging]:border-emerald-400",
    )}
  >
    {({ elicitation, resolveElicitation }) => (
      <>
        {/* Drop overlay uses group-data-* for styling */}
        <div className="hidden group-data-[dragging]:flex absolute inset-0 bg-emerald-50/90">
          <p>Drop files here</p>
        </div>

        {elicitation ? (
          <ElicitationUI
            request={elicitation}
            onResponse={resolveElicitation}
          />
        ) : (
          children
        )}
      </>
    )}
  </BaseComponent.Content>
);

// OK - styled sub-components can use context hook for deep access
const StyledTextarea = ({ placeholder }) => {
  const { value, setValue, handleSubmit, editorRef } = useMessageInputContext();
  return (
    <CustomEditor
      ref={editorRef}
      value={value}
      onChange={setValue}
      onSubmit={handleSubmit}
      placeholder={placeholder}
    />
  );
};
何时使用Context钩子 vs 渲染属性:
  • 渲染属性:当父包装器需要状态来变更行为时
  • Context钩子:当样式化子组件需要渲染属性未暴露的值时

Step 5: Wrap Sub-Components

步骤5:包装子组件

tsx
// Submit button
const SubmitButton = ({ className, children }) => (
  <BaseComponent.SubmitButton className={cn("w-10 h-10 rounded-lg", className)}>
    {({ showCancelButton }) =>
      children ?? (showCancelButton ? <Square /> : <ArrowUp />)
    }
  </BaseComponent.SubmitButton>
);

// Error
const Error = ({ className }) => (
  <BaseComponent.Error className={cn("text-sm text-destructive", className)} />
);

// Staged images - base pre-computes props array, just iterate
const StagedImages = ({ className }) => (
  <BaseComponent.StagedImages className={cn("flex gap-2", className)}>
    {({ images }) =>
      images.map((imageProps) => (
        <ImageBadge key={imageProps.image.id} {...imageProps} />
      ))
    }
  </BaseComponent.StagedImages>
);
tsx
// Submit button
const SubmitButton = ({ className, children }) => (
  <BaseComponent.SubmitButton className={cn("w-10 h-10 rounded-lg", className)}>
    {({ showCancelButton }) =>
      children ?? (showCancelButton ? <Square /> : <ArrowUp />)
    }
  </BaseComponent.SubmitButton>
);

// Error
const Error = ({ className }) => (
  <BaseComponent.Error className={cn("text-sm text-destructive", className)} />
);

// Staged images - base pre-computes props array, just iterate
const StagedImages = ({ className }) => (
  <BaseComponent.StagedImages className={cn("flex gap-2", className)}>
    {({ images }) =>
      images.map((imageProps) => (
        <ImageBadge key={imageProps.image.id} {...imageProps} />
      ))
    }
  </BaseComponent.StagedImages>
);

Step 6: Final Verification

步骤6:最终验证

Final Checks:
- [ ] No duplicate context creation
- [ ] No duplicate SDK hooks in root wrappers
- [ ] No duplicate state management or event handlers
- [ ] Base namespace imported and `Base.Root` used as wrapper
- [ ] `data-*` classes used for styling (with `group-data-*` for children)
- [ ] Render props used only for rendering behavior changes
- [ ] Base sub-components wrapped with styling
- [ ] Icon factories passed from styled layer to base hooks
- [ ] Visual sub-components and CSS variants stay in styled layer
Final Checks:
- [ ] No duplicate context creation
- [ ] No duplicate SDK hooks in root wrappers
- [ ] No duplicate state management or event handlers
- [ ] Base namespace imported and `Base.Root` used as wrapper
- [ ] `data-*` classes used for styling (with `group-data-*` for children)
- [ ] Render props used only for rendering behavior changes
- [ ] Base sub-components wrapped with styling
- [ ] Icon factories passed from styled layer to base hooks
- [ ] Visual sub-components and CSS variants stay in styled layer

What Belongs in Styled Layer

样式层应包含的内容

Icon Factories

图标工厂

When base hooks need icons, pass a factory function:
tsx
// Base hook accepts optional icon factory
export function useCombinedResourceList(
  providers: ResourceProvider[] | undefined,
  search: string,
  createMcpIcon?: (serverName: string) => React.ReactNode,
) {
  /* ... */
}

// Styled layer provides the factory
const resources = useCombinedResourceList(providers, search, (serverName) => (
  <McpServerIcon name={serverName} className="w-4 h-4" />
));
当基础钩子需要图标时,传入工厂函数:
tsx
// Base hook accepts optional icon factory
export function useCombinedResourceList(
  providers: ResourceProvider[] | undefined,
  search: string,
  createMcpIcon?: (serverName: string) => React.ReactNode,
) {
  /* ... */
}

// Styled layer provides the factory
const resources = useCombinedResourceList(providers, search, (serverName) => (
  <McpServerIcon name={serverName} className="w-4 h-4" />
));

CSS Variants

CSS变体

tsx
const inputVariants = cva("w-full", {
  variants: {
    variant: {
      default: "",
      solid: "[&>div]:shadow-xl [&>div]:ring-1",
      bordered: "[&>div]:border-2",
    },
  },
});
tsx
const inputVariants = cva("w-full", {
  variants: {
    variant: {
      default: "",
      solid: "[&>div]:shadow-xl [&>div]:ring-1",
      bordered: "[&>div]:border-2",
    },
  },
});

Layout Logic, Visual Sub-Components, Custom Data Fetching

布局逻辑、可视化子组件、自定义数据获取

These all stay in the styled layer. Base handles behavior; styled handles presentation.
这些内容都应保留在样式层中。基础组件处理行为,样式层处理展示。

Type Handling

类型处理

Handle ref type differences between base and styled components:
tsx
// Base context may have RefObject<T | null>
// Styled component may need RefObject<T>
<TextEditor ref={editorRef as React.RefObject<TamboEditor>} />
处理基础组件与样式化组件之间的Ref类型差异:
tsx
// Base context may have RefObject<T | null>
// Styled component may need RefObject<T>
<TextEditor ref={editorRef as React.RefObject<TamboEditor>} />

Anti-Patterns

反模式

  • Re-implementing base logic - if base handles it, compose it
  • Using render props for styling - prefer
    data-*
    classes; render props are for behavior changes
  • Duplicating context in wrapper - use base Root which provides context
  • Hardcoding icons in base hooks - use factory functions to keep styling in styled layer
  • 重复实现基础组件逻辑 - 如果基础组件已处理该逻辑,直接组合即可
  • 使用渲染属性进行样式设置 - 优先使用
    data-*
    类;渲染属性仅用于行为变更
  • 在包装器中重复创建Context - 使用提供Context的基础Root组件
  • 在基础钩子中硬编码图标 - 使用工厂函数将样式保留在样式层中