Loading...
Loading...
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".
npx skill4agent add tambo-ai/tambo creating-styled-wrappersbuilding-compound-components// 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>
);
};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 verificationuseTamboThreaduseTamboThreadInputReact.createContextimport { MessageInput as MessageInputBase } from "@tambo-ai/react-ui-base/message-input";// 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>
);
};data-*// 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}
/>
);
};// 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>
);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// 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" />
));const inputVariants = cva("w-full", {
variants: {
variant: {
default: "",
solid: "[&>div]:shadow-xl [&>div]:ring-1",
bordered: "[&>div]:border-2",
},
},
});// Base context may have RefObject<T | null>
// Styled component may need RefObject<T>
<TextEditor ref={editorRef as React.RefObject<TamboEditor>} />data-*