Today Canvas Kit

Render-mode contract

The four transports a TCK widget can reach a viewport through — Unbundled, Bundled, Embed, Iframe — and the theme propagation channel each uses.

A widget bundle can mount through one of four transports, each with a different trust boundary and theme-propagation channel. This page pins the taxonomy, the invariants each mode relies on, and where the frosted-frame chrome lives across them.

Sibling pages: container contract (the typed ContainerParams offer), visual container contract (the frosted-frame recipe), theme-injection contract (the single sanctioned read of prefers-color-scheme).

The four transports

ModeWhat loadsTrust boundaryUsed by
UnbundledWorkspace source via Vite (import './widget.tck.tsx')Same realm as host. Live HMR. Dev only.apps/host-playground workspace tiles, tck-preview Unbundled
BundledLocal .tckb artifact loaded via tck-bundle-server ingestSame realm as host. Exercises the canonical bundler output.tck-preview Bundled, apps/host-playground workspace + project sources
EmbedRemote widget.mjs loaded into the host's React treeSame realm as host. Production path on web.today-platform-web canvas-boot (/feed/*, /embed/today-page/v2)
Iframewidget.mjs loaded into a separate document inside an iframeCross-realm isolation by document boundary. Available for third-party widgets.macOS Bundle window (post-#46), tck-preview Iframe picker

The first three modes share one JavaScript realm; isolation is the Shadow DOM boundary established by <WidgetMount mode='inline'>. The fourth moves isolation to a document boundary.

Theme channel per mode

ModeInitial themeRuntime theme change
Unbundled<html data-theme><html data-theme> mutated in place; <WidgetMount> mirrors to each shadow host.
Bundled<html data-theme>Same as Unbundled.
Embed?theme=light|dark on URLpostMessage to canvas-boot → mountControl.setTheme(...) rewrites <html data-theme>.
Iframe?theme=light|dark on URLpostMessage cross-document → inner mountControl.setTheme(...).

Widgets MUST NOT read prefers-color-scheme directly. Theme is externally injected by the consumer — see theme-injection contract for the single sanctioned read inside the SDK.

Iframe transparency — the invariant

The iframe element is transparent by UA default. For an embedded document to remain transparent under all color-scheme combinations, the document MUST declare an explicit transparent background:

html,
body {
  margin: 0;
  padding: 0;
  background: transparent;
}

This is the structural rule that lets a dark-theme widget render correctly inside a light-scheme parent (or vice versa). The CSS Color Adjust spec § "Color Schemes and IFrames" mandates a UA-opaque canvas on color-scheme mismatch — except when the embedded document declares its own background. transparent is a valid declaration; it explicitly overrides the would-be UA opaque canvas.

The rule is emitted by getHostHeadNodes() on every embed-mode boot. If anyone removes that declaration, iframe-mode regresses immediately under mismatched-color-scheme conditions — no error, no warning, no test failure with today's coverage. The host-shell-emitted rule applies whether the consumer is the embed route, a feed page, or a third-party host.

Where the frosted frame lives

Per the visual container contract the host paints the frame and the widget renders content-only. Across modes:

  • Unbundled / Bundled / Embed — same realm, Shadow DOM per widget. The host's per-card <FeedCardSlot> (or <WidgetSlot>) wraps <WidgetMount mode='inline'> in a <FrostedFrame>. The shared reset sheet adopted into every shadow root defaults widget roots to transparent.
  • Iframe — the parent host renders the <FrostedFrame> around the <iframe> element; the inner document renders the widget content. The frosted-glass shadow cannot bleed outside the iframe element's rectangle — a known visual-fidelity tradeoff that iframe mode pays for cross-realm isolation. Acceptable for third-party widgets; preferred mode for first-party is inline.

Picking a mode

You areDefault modeWhen to switch
Writing a widget — iterating with HMRUnbundled (pnpm dev in tck-preview)Switch to Bundled to verify the artifact matches; switch to Iframe to test isolation explicitly.
Embedding in a web productEmbed (production), Bundled (dev fixtures)Switch to Iframe if loading third-party / untrusted widgets.
Building a native shellEmbed (single-widget WebView) or Bundled (offline .tckb)Switch to Iframe if hosting third-party widgets in one process.
Building a canvas workbench (Tier 2)Bundled for portable build verificationSwitch to Unbundled for an interactive HMR loop.

The mode is the consumer's call; the SDK contract is uniform across modes — same <WidgetMount> API, same WidgetCtx, same externals whitelist, same Shadow DOM (inline mode) or iframe document (iframe mode).

On this page