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 in the side rail. 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 card renders inline in the safe-area-right side rail. Placement is client-side; the model does not choose the slot. The scheduling_embed card is config-gated: it renders only when the PAL supplies a scheduling provider and a scheduling_url. Other runtime behavior:

Props

All props are optional; <MagicCanvas /> with no props is fully functional.

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

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: 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.