Skip to main content

Canvas Card

The agent can draw a chart or a layout for the user: one self-contained HTML/SVG fragment that the SDK renders as a canvas card. The built-in <Chatbot> handles it; there is nothing to wire.

The Markup Is Untrusted

Canvas content is generated per conversation by the model and may contain <style> and <script>. Injecting it into the host page would hand that page's origin — its cookies, its storage, its DOM — to content the host did not author.

Rendering it yourself requires isolation

It must go in an isolated browsing context with scripting allowed but same-origin access denied. In a browser that means an iframe sandboxed without allow-same-origin, fed through srcdoc.

That isolation also keeps the fragment off the network, which is expected — a canvas is authored to be self-contained.

The SDK's built-in CanvasTemplate already does exactly this. You only deal with it if you bypass the built-in rendering entirely.

Types

interface CanvasMessageTemplate {
type: "CANVAS";
title?: string; // card title; the renderer shows only this
canvas: { html: string };
}

title is the only title source — do not extract one from the markup.

Folded into the thread it takes this shape:

type ConversationCanvasMessage = {
type: "canvas";
messageId: string;
html: string;
title?: string;
isDrawing: boolean; // true = still arriving; false = the authoritative fragment landed
traceId?: string;
};

Streaming: the Deltas Only Exist to Show It Taking Shape

A canvas travels over asgard.message.canvas.start / .delta / .complete.

The html on complete is authoritative. The intermediate deltas exist purely so the card can be shown taking shape; a client that ignored every delta renders the identical document from that field alone.

While isDrawing is true, html is a prefix that is still growing, not a complete document.

Theming

The canvas lives in an iframe and cannot reach the host's CSS. The SDK resolves seven concrete values with resolveCanvasTheme() and hands them over:

FieldPurpose
fg / bgForeground / background
accentEmphasis: the data itself (bars, lines, highlights) and anything clickable
mutedDe-emphasized text: labels, captions, axes
borderHairlines, card outlines, dividers
padding / selectionPadding and selection color

The first five names are a contract with the backend's show_canvas tool description — do not rename them.

It reads computed colors, not CSS variables

resolveCanvasTheme() reads the computed color / background-color of real elements rather than custom properties like --fg / --surface.

That was learned the expensive way: the variable-reading version missed entirely on a design system that calls its main text color --text-primary, so the text color fell back to near-white while --surface resolved to #ffffff in light mode — white on white, every pixel present, nothing visible, and readable only by luck on a dark host.

A computed color has none of those problems: it is always a resolved rgb(...), always reflects the theme in force, and requires no knowledge of what the host calls its tokens or what format they are in. The background also walks up the ancestor chain — the card itself may be transparent, and an iframe with no stated background is white.

To override, CanvasTemplate takes theme?: Partial<ResolvedCanvasTheme>.

See Also