<MagicCanvas /> is the @tavus/cvi-ui component that renders Magic Canvas cards inside your own React app. It listens for Canvas invocations, renders each card, and posts user interactions to Tavus and your webhook.
The Tavus-hosted embed and widget render Canvas automatically and do not require this SDK.
Prerequisites
Attach themagic_canvas skill to your PAL; it enables every component with defaults, and every video conversation on that PAL gets Canvas automatically with nothing to declare on the client. See the Magic Canvas overview.
Installation
@tavus/cvi-ui is a CLI that copies component source into your app, not a runtime dependency.
Initialize cvi-ui (skip if already initialized)
cvi-components.json and installs the shared dependencies:
@daily-co/daily-react, @daily-co/daily-js, and jotai.Creating the Conversation
The PAL’s Magic Canvas skill provides the Canvas actions; the create call needs nothing Canvas-specific:Conversations with no rendering surface skip Canvas:
audio_only
conversations, text chats, and conversations with a meeting_url (Zoom,
Teams, Meet) get no Canvas actions: no error and no cards.Mounting the Component
Mount<MagicCanvas /> as a sibling of <Conversation /> inside one CVIProvider (the Daily provider, exactly one per call); both listen on the same call object:
Placement
<MagicCanvas /> renders a fixed full-viewport overlay (position: fixed; inset: 0; pointer-events: none, high z-index) and places each card via the slot system below. The overlay never blocks clicks; each card re-enables pointer-events for itself.
Cards cannot mount in your own containers: renderComponent swaps what renders, not where (Bring your own renderer); className restyles the overlay without moving it; onLayoutEffectChange reports the video shift for side panels.
To pin the overlay inside a container instead of the viewport, the examples/vite-app demo uses a CSS override:
position: relative wrapper around <Conversation /> and <MagicCanvas className="canvas-in-player" />. The side-rail card then stays inside the box.
Card Slots
Every GA card renders inline in a side rail. The model can place a card in one of two slots:| Slot | Default for |
|---|---|
safe-area-right | Every card (the effective default) |
safe-area-left | None |
safe-area-right or safe-area-left. The scheduling_embed card is config-gated: it renders only when the PAL supplies a scheduling provider and a scheduling_url.
Other runtime behavior:
| Condition | Behavior |
|---|---|
| Multiple cards | Only one card is on screen at a time; a new show or update replaces the current card. |
| Inline card height | Sized to the height the card reports, clamped between a 48px floor and a 720px maximum. |
Props
All props are optional;<MagicCanvas /> with no props is fully functional.
| Field | Type | Required | Description |
|---|---|---|---|
className | string | ❌ | Appended to the overlay’s root class list. Restyles the overlay; does not change where cards render. |
onInteraction | (event: CanvasInteractionEvent) => void | Promise<void> | ❌ | Called for every user interaction (submit, skip, dismiss) before it is sent to Tavus. |
onError | (event: CanvasErrorEvent) => void | ❌ | Called for malformed configs, failed posts, and renderer errors. |
onLayoutEffectChange | (layout: CanvasSidecarLayout) => void | ❌ | Called when the side panel opens, closes, or moves. |
renderComponent | CanvasRenderRegistry | ❌ | Registry of your own React renderers, keyed by "<component>@<version>". See Bring your own renderer. |
onInteraction
on_interaction_callback_failed error event fires and the post still proceeds; the interaction reaches your webhook either way.
onError
| Code | Meaning |
|---|---|
malformed_canvas_config | A Canvas action carried a config the SDK couldn’t parse. |
missing_tool_call_id | A Canvas invocation arrived without a tool_call_id. |
invalid_tool_arguments | The action’s arguments failed to parse. |
missing_interaction_metadata | A card emitted an interaction without the required metadata. |
interaction_normalization_failed | An interaction couldn’t be turned into a postable payload. |
interaction_post_failed | The POST to Tavus failed or timed out (5 seconds). |
on_interaction_callback_failed | Your onInteraction handler threw. |
bridge_connect_failed | A sandboxed card failed to establish its message bridge. |
send_tool_input_failed | A live update couldn’t be relayed to a card. |
onLayoutEffectChange
When a side-slot card opens, the canvas reserves a 448px panel and reports how far centered video content should shift to clear it:
active is false below that, and the video stays centered.
Interaction Delivery
Every interaction is posted to:interaction_post_failed error event. Successful interactions arrive at your conversation webhook as canvas.interaction events, and the PAL responds in the conversation.
Bring Your Own Renderer
By default, cards render inside a sandboxed iframe; sandboxed component UIs load from Tavus-approved hosts only. PassrenderComponent (a registry keyed by "<component>@<version>") to render a component natively instead:
component, version, and args (runtime/layout keys already stripped), plus callbacks:
| Callback | Description |
|---|---|
submit(interaction) | Reports a user interaction: { type?, value, metadata? }; type defaults to "submit". Runs normalization, onInteraction, the interaction POST, and auto-dismiss, identical to the iframe path. |
sendContext({ content?, structuredContent? }) | Appends model context to the conversation. |
respond(text) | Sends a free-text reply the PAL responds to. |
onError(error) | Surfaces a renderer-side error through the host’s onError. |
canvas.interaction webhook, and auto-dismiss behavior are identical for both paths, and native cards use the same slot and layout model as iframe cards. Components without a matching registry entry keep using the sandboxed iframe.
Complete Example
Adapted fromexamples/vite-app in the cvi-ui repository. The installed tavus-api route expects a JSON body of { "action": "create", "params": { ... } }, forwards params to Tavus verbatim, and is assumed mounted at POST /api/tavus.
main.tsx
App.tsx

