creating-styled-wrappers

Styling Compound Wrappers

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "creating-styled-wrappers" with this command: npx skills add tambo-ai/tambo/tambo-ai-tambo-creating-styled-wrappers

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.

Core Principle: Compose, Don't Duplicate

Styled wrappers should compose base components, not re-implement their logic.

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

Step 1: Identify Duplicated Logic

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

Step 2: Import Base Components

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

Step 3: Wrap with Base Root

Replace custom context/state management with the base Root:

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

State access follows a hierarchy — use the simplest option that works:

  • Data attributes (preferred for styling) — base components expose data-* attributes

  • Render props (for behavior changes) — use when rendering different components

  • Context hooks (for sub-components) — OK for styled sub-components needing deep context access

// 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 }) => (
  &#x3C;>
    {/* Drop overlay uses group-data-* for styling */}
    &#x3C;div className="hidden group-data-[dragging]:flex absolute inset-0 bg-emerald-50/90">
      &#x3C;p>Drop files here&#x3C;/p>
    &#x3C;/div>

    {elicitation ? (
      &#x3C;ElicitationUI
        request={elicitation}
        onResponse={resolveElicitation}
      />
    ) : (
      children
    )}
  &#x3C;/>
)}

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

Step 5: Wrap Sub-Components

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

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:

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

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:

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

generative-ui

No summary provided by upstream source.

Repository SourceNeeds Review
General

add-components-to-registry

No summary provided by upstream source.

Repository SourceNeeds Review
General

threads

No summary provided by upstream source.

Repository SourceNeeds Review