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 regular registered components that Tambo generates and renders from scratch 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.
import { withInteractable } from "@tambo-ai/react";
import { z } from "zod";
// Your existing component
function Note({ title, content, color = "white" }) {
return (
<div className={`note note-${color}`}>
<h3>{title}</h3>
<p>{content}</p>
</div>
);
}
// Make it interactable
const InteractableNote = withInteractable(Note, {
componentName: "Note",
description:
"A simple note component that can display title and content with different colors",
propsSchema: z.object({
title: z.string(),
content: z.string(),
color: z.enum(["white", "yellow", "blue", "green"]).optional(),
}),
});
// Another simple component
function Counter({ count, label }) {
return (
<div className="counter">
<span>
{label}: {count}
</span>
</div>
);
}
const InteractableCounter = withInteractable(Counter, {
componentName: "Counter",
description: "A simple counter that displays a label and count value",
propsSchema: z.object({
count: z.number(),
label: z.string(),
}),
});
// Place both components in your app
function Page() {
return (
<div>
<InteractableNote
title="Welcome"
content="This is a simple note that Tambo can update!"
color="yellow"
/>
<InteractableCounter count={42} label="Items" />
</div>
);
}Now Tambo is able to read and update these components 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"
- "Set the counter to 100"
- "Change the counter label to 'Tasks Completed'"
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 />
<InteractableCounter />
</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"
- "Make the counter red and set it to 100"
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." />
<InteractableCounter count={42} />
</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 and Counters, hide other component types
const allowedTypes = ["Note", "Counter"];
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 updatescount, 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'spropsSchemaand accepts partials.
These tools enable the AI to discover what's on the page and perform targeted updates.
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
undefinedvalues, 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.