Skip to main content
<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 the magic_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.
1

Initialize cvi-ui (skip if already initialized)

Creates cvi-components.json and installs the shared dependencies: @daily-co/daily-react, @daily-co/daily-js, and jotai.
2

Add the component

Copies the Magic Canvas source into src/components/cvi/components/magic-canvas (app/components/cvi/components/magic-canvas if your project has no src directory); the import paths below resolve from there. If you don’t have a conversation UI yet, also run:

Creating the Conversation

The PAL’s Magic Canvas skill provides the Canvas actions; the create call needs nothing Canvas-specific:
Create conversations from your server; never ship your Tavus API key in the client bundle. npx @tavus/cvi-ui@latest add tavus-api installs a server route for Next.js, Remix, and TanStack Start that keeps the key server-side.
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:
The component renders nothing until the PAL shows a card.

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:
Requires a 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:
SlotDefault for
safe-area-rightEvery card (the effective default)
safe-area-leftNone
No other slots are offered to the model; any other placement hint is clamped to an inline side rail, so cards always render in 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:
ConditionBehavior
Multiple cardsOnly one card is on screen at a time; a new show or update replaces the current card.
Inline card heightSized 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.
FieldTypeRequiredDescription
classNamestringAppended 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) => voidCalled for malformed configs, failed posts, and renderer errors.
onLayoutEffectChange(layout: CanvasSidecarLayout) => voidCalled when the side panel opens, closes, or moves.
renderComponentCanvasRenderRegistryRegistry of your own React renderers, keyed by "<component>@<version>". See Bring your own renderer.

onInteraction

The handler is awaited before the interaction is posted to Tavus. If it throws, an on_interaction_callback_failed error event fires and the post still proceeds; the interaction reaches your webhook either way.

onError

CodeMeaning
malformed_canvas_configA Canvas action carried a config the SDK couldn’t parse.
missing_tool_call_idA Canvas invocation arrived without a tool_call_id.
invalid_tool_argumentsThe action’s arguments failed to parse.
missing_interaction_metadataA card emitted an interaction without the required metadata.
interaction_normalization_failedAn interaction couldn’t be turned into a postable payload.
interaction_post_failedThe POST to Tavus failed or timed out (5 seconds).
on_interaction_callback_failedYour onInteraction handler threw.
bridge_connect_failedA sandboxed card failed to establish its message bridge.
send_tool_input_failedA live update couldn’t be relayed to a card.
Errors never throw into your render tree; they all arrive here.

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:
Nothing applies the shift for you; wire this callback if you render your own video layout. The shift only activates on viewports 900px and wider; active is false below that, and the video stays centered.

Interaction Delivery

Every interaction is posted to:
The post times out after 5 seconds; failures fire an 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. Pass renderComponent (a registry keyed by "<component>@<version>") to render a component natively instead:
Your renderer receives the validated component, version, and args (runtime/layout keys already stripped), plus callbacks:
CallbackDescription
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.
The interaction payload, 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 from examples/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
Start a conversation and ask the PAL to “show me a multiple choice question.” A card appears next to the PAL video, and the console logs the interaction.