Skip to main content
A widget runs at an opaque origin inside <iframe sandbox="allow-scripts" srcdoc="…"> — no allow-same-origin, no network egress (the frame inherits the notch’s CSP), no host access. postMessage is the only channel, and this page is its complete wire format. Two widget surfaces speak slightly different dialects:

Host → widget

Sent once, when the frame loads:
  • data is the block’s data prop verbatim (≤ 16,384 chars serialized).
  • args is the pending tool call’s argument object — confirmation widgets only. A result widget must carry everything it needs in data.
  • theme.mode is the host’s theme. Style from this, never from prefers-color-scheme (the OS theme is the wrong signal inside the notch).
  • radius is the corner this host cuts your card at, in px. It is normally 26, but a surface whose clip is concentric with a different shell sends its own value (the pill clips at 25). The kit applies it to --k-radius for you; hand-rolled documents should do the same. See The corner.
  • The message is posted with target origin "*" — necessarily, since a sandboxed document’s origin is opaque and can’t be named. Isolation comes from the sandbox, not the target origin.

Widget → host

Every message is accepted only when event.source is the widget’s own contentWindow — a message from any other frame is ignored.

voiceos:resize

The host clamps to 60–420 px and animates (height 160ms ease). Non-numeric heights are ignored. Report only real measurements — never while document.hidden, zero-width, or under 8 px, or one bad sample pins the card at the 60 px floor.

voiceos:openUrl

https only — anything else is dropped. Opens in the user’s browser (noopener,noreferrer). Route your links through this: the sandbox blocks navigation, so an unrouted <a> is a dead click.

voiceos:updateInput (confirmation widgets only)

Stages an edit to a pending tool argument; staged edits ride the user’s approval and replace the agent’s proposed values. Anything malformed is silently dropped. Result widgets have no pending call — the message is ignored there.

What the Widget Kit does for you

Documents built with the Widget Kit implement the widget side automatically:
  • Resize: a ResizeObserver plus load/settle timers report height changes > 1 px, with the collapsed-measurement guard built in.
  • Hydration: on init, the theme lands as data-k-theme on the root, and every [data-k-key] / [data-voiceos-key] element is filled from args.
  • Input relay: a delegated input listener posts voiceos:updateInput for any keyed element.
  • Link routing: clicks on [data-k-link] elements and plain <a href="https://…"> anchors are intercepted and sent via voiceos:openUrl.

Interaction & focus

The notch is a click-through overlay window; the host grabs keyboard focus when the user mouses down inside your frame, so typing in widget inputs works. There’s nothing to implement — but know that focus follows pointer, not tab order, across the frame boundary.

Security invariants (the short version)

  1. Opaque origin: no cookies, storage, parent DOM, or host APIs.
  2. No network egress from inside the frame — assets must be inlined.
  3. Height is clamped on render and on every resize.
  4. Message source identity is verified on every widget→host message.
  5. No approve leg exists. Nothing a widget posts can execute a tool.
  6. Only integration tool results and manifest confirmations can carry widgets — a model-authored card cannot (the parser drops widget blocks unless the integration path explicitly allows them).