creating-styled-wrappers
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseStyling Compound Wrappers
样式化复合包装组件
Create styled wrapper components that compose headless base compound components. This skill complements (which builds the base primitives) by focusing on how to properly consume and wrap them with styling and additional behavior.
building-compound-componentsReal-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 verificationStep 1: Identify Duplicated Logic
步骤1:识别重复逻辑
Look for patterns that indicate logic should come from base:
- SDK hooks (,
useTamboThread, etc.)useTamboThreadInput - 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:
- Data attributes (preferred for styling) — base components expose attributes
data-* - Render props (for behavior changes) — use when rendering different components
- 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
状态访问遵循层级结构——使用最简单可行的方案:
- 数据属性(样式首选)——基础组件会暴露属性
data-* - 渲染属性(用于行为变更)——在渲染不同组件时使用
- 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 layerFinal 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 layerWhat 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 classes; render props are for behavior changes
data-* - 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组件
- 在基础钩子中硬编码图标 - 使用工厂函数将样式保留在样式层中