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.
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:
| Field | Purpose |
|---|---|
fg / bg | Foreground / background |
accent | Emphasis: the data itself (bars, lines, highlights) and anything clickable |
muted | De-emphasized text: labels, captions, axes |
border | Hairlines, card outlines, dividers |
padding / selection | Padding and selection color |
The first five names are a contract with the backend's show_canvas tool description — do
not rename them.
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
- Message Templates — the full template catalog
- Question Card — the other agent-initiated interactive card