Loading...

Interactable Components

Allow Tambo to update your pre-placed components

When you want to place specific components on screen rather than letting Tambo choose which to show, but still want to allow your users to interact with them using natural language, use Tambo's Interactable components.

Unlike generative components that Tambo creates on-demand when responding to messages, interactable components are pre-placed by you while still allowing Tambo to modify their props.

Creating Interactable Components

The easiest way to make a component interactable to Tambo is by using withInteractable. Pass in your component, a name, a description, and the props schema, and get an interactable version of your component that Tambo knows about.

  1. Build the presentational component. Use useEffect to keep any local UI state aligned with incoming props from Tambo.

    import { useEffect, useState } from "react";
    
    type NoteProps = {
      title: string;
      content: string;
      color?: "white" | "yellow" | "blue" | "green";
    };
    
    export function Note({ title, content, color = "yellow" }: NoteProps) {
      const [draftContent, setDraftContent] = useState(content);
    
      useEffect(() => {
        setDraftContent(content);
      }, [content]);
    
      return (
        <section className={`note note-${color}`}>
          <h3>{title}</h3>
          <textarea
            value={draftContent}
            onChange={(event) => setDraftContent(event.currentTarget.value)}
          />
        </section>
      );
    }
  2. Register it with withInteractable. This tells Tambo the component name, description, and which props it can safely edit.

    import { withInteractable } from "@tambo-ai/react";
    import { z } from "zod";
    import { Note } from "./note";
    
    export const InteractableNote = withInteractable(Note, {
      componentName: "Note",
      description:
        "A simple note that can change title, content, and background color",
      propsSchema: z.object({
        title: z.string(),
        content: z.string(),
        color: z.enum(["white", "yellow", "blue", "green"]).optional(),
      }),
    });
  3. Render the interactable version in your app. Tambo now sees the note and can change its props in place.

    export function Page() {
      return (
        <main>
          <InteractableNote
            title="Release plan"
            content="Ask Tambo to keep this note up to date."
            color="yellow"
          />
        </main>
      );
    }
Show full example
import { useEffect, useState } from "react";
import { withInteractable } from "@tambo-ai/react";
import { z } from "zod";

type NoteProps = {
  title: string;
  content: string;
  color?: "white" | "yellow" | "blue" | "green";
};

function Note({ title, content, color = "yellow" }: NoteProps) {
  const [draftContent, setDraftContent] = useState(content);

  useEffect(() => {
    setDraftContent(content);
  }, [content]);

  return (
    <section className={`note note-${color}`}>
      <h3>{title}</h3>
      <textarea
        value={draftContent}
        onChange={(event) => setDraftContent(event.currentTarget.value)}
      />
    </section>
  );
}

const InteractableNote = withInteractable(Note, {
  componentName: "Note",
  description:
    "A simple note that can change title, content, and background color",
  propsSchema: z.object({
    title: z.string(),
    content: z.string(),
    color: z.enum(["white", "yellow", "blue", "green"]).optional(),
  }),
});

export default function Page() {
  return (
    <main>
      <InteractableNote
        title="Release plan"
        content="Ask Tambo to keep this note up to date."
        color="yellow"
      />
    </main>
  );
}

No Registration Required

Unlike regular components that need to be registered in the TamboProvider components array, interactable components are automatically registered when they mount. You don't need to add them to your components array.

Want Inline Generation Too?

If you want Tambo to be able to both modify pre-placed interactable instances AND generate new generative component instances inline, you'll need to register the component normally in the TamboProvider as well.

This baseline shows the key workflow:

  • Build presentational components that accept props.
  • Use useEffect inside the component to mirror incoming prop changes onto any local UI state (like the textarea draft).
  • Register them with withInteractable, providing a schema so Tambo knows which props it can mutate.
  • Render the interactable version in your app; Tambo can now update those props in place.

The textarea edits are local to the component, which keeps the example lightweight.

Need two-way sync?

Want the user's edits to flow back to Tambo? Follow the step-by-step guide in Two-way state syncing for interactables.

Now Tambo is able to read and update the note in-place when responding to messages. Users can ask things like:

  • "Change the note title to 'Important Reminder'"
  • "Update the note content to 'Don't forget the meeting at 3pm'"
  • "Make the note blue"
  • "Summarize the note I pinned earlier"

InteractableConfig

When using withInteractable, you provide a configuration object describing the component to Tambo:

interface InteractableConfig {
  componentName: string; // Name for Tambo to reference
  description: string; // Description of what the component does
  propsSchema: z.ZodTypeAny; // Schema of props for Tambo to generate
}

How it Works

For each component marked as interactable using withInteractable, behind the scenes Tambo stores a state object representing the props of the component, and registers a tool to update that object.

When Tambo decides to update an interactable component while responding to a message, it uses that component's 'update' tool, which updates the state and triggers a re-render of your wrapped component.

Integration with Tambo Provider

Make sure your app is wrapped with the <TamboProvider/>.

import { TamboProvider } from "@tambo-ai/react";

function App() {
  return (
    <TamboProvider>
      {/* Your app with interactable components */}
      <InteractableNote />
    </TamboProvider>
  );
}

This creates a truly conversational interface where users can modify your UI through natural language, making your applications more accessible and user-friendly.

Automatic Context Awareness

When you use TamboInteractableProvider, your interactable components are automatically included in the AI's context. This means:

  • The AI knows what components are currently on the page
  • Users can ask "What's on this page?" and get a comprehensive answer
  • The AI can see the current state (props) of all interactable components
  • Component changes are reflected in real-time

No additional setup required - this context is provided automatically and can be customized or disabled if needed.

Example Interactions

With interactable components on the page, users can ask:

  • "What components are available?"
  • "Change the note title to 'Important Reminder'"
  • "Show me the current state of all my components"
  • "Summarize the note I pinned earlier"

Customizing Automatic Context

The automatic context can be disabled, enabled selectively, or customized to show only specific information.

Disable Globally

To disable interactables context across your entire app:

<TamboProvider
  apiKey={apiKey}
  contextHelpers={{
    // Disable interactables context globally
    interactables: () => null,
  }}
>
  <TamboInteractableProvider>
    {/* Interactables context is disabled, but components still work */}
    <InteractableNote title="Hidden from AI" />
  </TamboInteractableProvider>
</TamboProvider>

Enable Locally (Override Global Disable)

If you've disabled it globally but want to enable it for a specific page or section:

function SpecificPage() {
  const { addContextHelper } = useTamboContextHelpers();
  const snapshot = useCurrentInteractablesSnapshot();

  React.useEffect(() => {
    // Re-enable interactables context for this page only
    const helper = () => {
      if (snapshot.length === 0) return null;

      return {
        description: "Interactable components on this page that you can modify",
        components: snapshot.map((component) => ({
          id: component.id,
          componentName: component.name,
          description: component.description,
          props: component.props,
          propsSchema: component.propsSchema ? "Available" : "Not specified",
        })),
      };
    };

    addContextHelper("interactables", helper);
  }, [addContextHelper, snapshot]);

  return (
    <TamboInteractableProvider>
      {/* Context is now enabled for this page */}
      <InteractableNote title="Visible to AI" />
    </TamboInteractableProvider>
  );
}

Custom Context (IDs Only Example)

Sometimes you might want to share summary information and have the AI request the full context when needed.

This is an example of how to only IDs and names with every message:

import {
  useCurrentInteractablesSnapshot,
  useTamboContextHelpers,
} from "@tambo-ai/react";

function IdsOnlyInteractables() {
  const { addContextHelper } = useTamboContextHelpers();
  const snapshot = useCurrentInteractablesSnapshot();

  React.useEffect(() => {
    const idsOnlyHelper = () => {
      if (snapshot.length === 0) return null;

      return {
        description: "Available interactable component ids.",
        components: snapshot.map((component) => ({
          id: component.id,
          componentName: component.name,
          // Deliberately omit props.
        })),
      };
    };

    // Override the default helper with our ids only version
    addContextHelper("interactables", idsOnlyHelper);
  }, [addContextHelper, snapshot]);

  return null; // This component just sets up the context helper
}

// Usage
<TamboInteractableProvider>
  <PrivacyFriendlyInteractables />
  <InteractableNote title="Not visible unless requested." />
</TamboInteractableProvider>;

Filter by Component Type

Maybe you only want to show certain types of components.

Here is an example of how you could filter by component type:

function FilteredInteractablesContext() {
  const { addContextHelper } = useTamboContextHelpers();
  const snapshot = useCurrentInteractablesSnapshot();

  React.useEffect(() => {
    const filteredHelper = () => {
      // Only show Notes, hide other component types
      const allowedTypes = ["Note"];
      const filteredComponents = snapshot.filter((component) =>
        allowedTypes.includes(component.name),
      );

      if (filteredComponents.length === 0) return null;

      return {
        description: "Available interactable components (filtered)",
        components: filteredComponents.map((component) => ({
          id: component.id,
          componentName: component.name,
          props: component.props,
        })),
      };
    };

    addContextHelper("interactables", filteredHelper);
  }, [addContextHelper, snapshot]);

  return null;
}

Partial Updates (Property Replacement)

Interactable component props are updated via partial updates. When an update occurs, only the provided top-level props are replaced in the component's existing props. This uses property replacement behavior:

  • Providing { count: 5 } only updates count, leaving other props unchanged.
  • Providing nested objects replaces that nested object entirely, potentially losing other properties within that object.

Example property replacement behavior:

// Original props
{
  title: "Original Title",
  config: {
    theme: "light",
    language: "en",
    features: { notifications: true, analytics: false },
  },
}

// Update with a nested object that omits some keys
{
  config: { theme: "dark" }
}

// Resulting props (config object is completely replaced)
{
  title: "Original Title",
  config: {
    theme: "dark",
    // language and features are now undefined because the entire config object was replaced
  },
}

Best practice for nested updates: Since nested objects are completely replaced, if you need to update a deeply nested value but keep the rest, provide the full nested object for that branch.

// Proper nested update (preserves other nested keys)
{
  config: {
    theme: "light",
    language: "en",
    features: {
      notifications: true,
      analytics: false,
    },
  },
}

Update Results and Errors

Updates return a string status:

  • "Updated successfully" for successful updates when props actually change
  • "No changes needed - all provided props are identical to current values" when no props change
  • "Error: Component with ID <id> not found" when the target does not exist
  • "Warning: No props provided for component with ID <id>." when the update object is empty/null/undefined

Auto-registered Tools for Interactables

When there are interactable components present, the following tools are registered automatically to help the AI reason about and modify your UI:

  • get_all_interactable_components — Returns all interactable components with their current props.
  • get_interactable_component_by_id — Returns a specific interactable component by id.
  • remove_interactable_component — Removes a component from the interactables list.
  • update_interactable_component_<id> — Updates the props for a specific component id using partial props. The argument schema is derived from the component's propsSchema and accepts partials.

These tools enable the AI to discover what's on the page and perform targeted updates.

Accessing Component Context from Child Components

When building child components inside an interactable (like inline editors, toolbars, or custom controls), use useTamboCurrentComponent to access the parent component's metadata.

Related Hooks

Use useTamboCurrentComponent when you only need component information. Use useTamboCurrentMessage when you need the complete message context, including thread ID, timestamps, and component state.

useTamboCurrentComponent

The useTamboCurrentComponent hook provides access to component metadata from any nested child component. It works seamlessly with both inline rendered components and interactable components.

import { useTamboCurrentComponent } from "@tambo-ai/react";

function InlineEditor() {
  const component = useTamboCurrentComponent();

  if (!component) {
    return null; // Not inside a component context
  }

  return (
    <div className="inline-editor">
      <h4>Editing: {component.componentName}</h4>
      {component.interactableId && (
        <span className="badge">ID: {component.interactableId}</span>
      )}
      {component.description && (
        <p className="description">{component.description}</p>
      )}
    </div>
  );
}

Return Value

The hook returns an object with the following fields, or null if used outside a component context:

FieldTypeDescription
componentNamestring | undefinedThe component's registered name
propsRecord<string, any> | undefinedThe component's current props
interactableIdstring | undefinedUnique identifier (only for interactable components)
descriptionstring | undefinedComponent description (only for interactable components)

How It Works

When you wrap a component with withInteractable, it automatically creates a TamboMessageProvider that makes component metadata available to all child components. This means any component nested inside can access the parent's context using this hook.

Usage Patterns

Pattern 1: Inline Editor with Component Context

Build an inline editor that automatically adapts to the component it's embedded in:

import { useTambo, useTamboCurrentComponent } from "@tambo-ai/react";
import { useState } from "react";

function InlineAIEditor() {
  const component = useTamboCurrentComponent();
  const { sendThreadMessage } = useTambo();
  const [prompt, setPrompt] = useState("");
  const [isEditing, setIsEditing] = useState(false);

  if (!component) return null;

  const handleEdit = async () => {
    await sendThreadMessage(prompt, {
      additionalContext: {
        inlineEdit: {
          componentId: component.interactableId,
          componentName: component.componentName,
          description: component.description,
          currentProps: component.props,
        },
      },
    });
    setPrompt("");
    setIsEditing(false);
  };

  return (
    <div className="inline-editor">
      {!isEditing ? (
        <button onClick={() => setIsEditing(true)}>✨ Edit with AI</button>
      ) : (
        <div className="editor-form">
          <input
            value={prompt}
            onChange={(e) => setPrompt(e.target.value)}
            placeholder={`Edit ${component.componentName}...`}
          />
          <button onClick={handleEdit}>Apply</button>
          <button onClick={() => setIsEditing(false)}>Cancel</button>
        </div>
      )}
    </div>
  );
}

// Use in any interactable component:
const InteractableCard = withInteractable(Card, {
  componentName: "Card",
  description: "A card component with title and content",
  propsSchema: cardSchema,
});

function Card({ title, content }) {
  return (
    <div className="card">
      <h3>{title}</h3>
      <p>{content}</p>
      <InlineAIEditor /> {/* Automatically knows it's in a Card */}
    </div>
  );
}

Pattern 2: Component-Aware Toolbar

Create a reusable toolbar that adapts based on the component it's in:

import { useTamboCurrentComponent } from "@tambo-ai/react";

function ComponentToolbar() {
  const component = useTamboCurrentComponent();

  if (!component?.interactableId) {
    return null; // Only show for interactable components
  }

  const handleDuplicate = () => {
    // Use component.props to create a duplicate
    console.log("Duplicating", component.componentName, component.props);
  };

  const handleDelete = () => {
    // Use component.interactableId to delete
    console.log("Deleting", component.interactableId);
  };

  return (
    <div className="toolbar">
      <span className="component-name">{component.componentName}</span>
      <button onClick={handleDuplicate}>Duplicate</button>
      <button onClick={handleDelete}>Delete</button>
    </div>
  );
}

// Works in any interactable component without modifications
function Note({ title, content }) {
  return (
    <div>
      <ComponentToolbar />
      <h3>{title}</h3>
      <p>{content}</p>
    </div>
  );
}

Pattern 3: Dynamic UI Based on Component State

Show different controls based on the component's current props:

import { useTamboCurrentComponent } from "@tambo-ai/react";

function StatusBadge() {
  const component = useTamboCurrentComponent();

  if (!component?.props) return null;

  // Access current props to show relevant UI
  const status = component.props.status;
  const priority = component.props.priority;

  return (
    <div className="status-bar">
      {status && <Badge variant={status}>{status}</Badge>}
      {priority && <PriorityIcon level={priority} />}
      <span className="component-type">{component.componentName}</span>
    </div>
  );
}

const InteractableTask = withInteractable(Task, {
  componentName: "Task",
  description: "A task item",
  propsSchema: z.object({
    title: z.string(),
    status: z.enum(["todo", "in-progress", "done"]),
    priority: z.enum(["low", "medium", "high"]).optional(),
  }),
});

function Task({ title, status, priority }) {
  return (
    <div className="task">
      <StatusBadge /> {/* Automatically shows correct status */}
      <h4>{title}</h4>
    </div>
  );
}

Key Concepts

When to Use interactableId

  • Identifying specific component instances for updates/deletion
  • Tracking components across renders
  • API calls that need a unique identifier

When to Use componentName

  • Conditional UI based on component type
  • Type-specific behaviors or styling
  • Component-type-aware suggestions

Accessing Props

  • Always check if component.props exists before accessing
  • Props are read-only - use state or callbacks to modify
  • Useful for displaying current values or conditional logic

Snapshot Hook: useCurrentInteractablesSnapshot

Use this hook to read the current interactables without risking accidental mutation of internal state. It returns a cloned snapshot of each item and its props.

import {
  useCurrentInteractablesSnapshot,
  useTamboContextHelpers,
} from "@tambo-ai/react";

function InteractablesContextSummary() {
  const { addContextHelper } = useTamboContextHelpers();
  const snapshot = useCurrentInteractablesSnapshot();

  React.useEffect(() => {
    const helper = () => {
      if (snapshot.length === 0) return null;
      return {
        description: "Interactable components currently on screen",
        components: snapshot.map((c) => ({
          id: c.id,
          componentName: c.name,
          props: c.props,
        })),
      };
    };

    addContextHelper("interactables", helper);
  }, [addContextHelper, snapshot]);

  return null;
}

Practical Tips

  • For nested updates, provide the complete nested object to avoid unintended undefined values, since nested objects are completely replaced.
  • Arrays are replaced entirely when provided in partial updates.
  • If you need fine-grained nested updates, structure your props to keep critical nested branches small and independent.
  • The property replacement behavior is predictable and explicit - you always know exactly what will be updated.

With these tools and behaviors, you can confidently let Tambo adjust parts of your UI through natural language while retaining predictable update semantics.