> ## Documentation Index
> Fetch the complete documentation index at: https://docs.voiceos.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Widget bridge reference

> The exact postMessage protocol between VoiceOS and a sandboxed widget.

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.

```mermaid theme={null}
sequenceDiagram
    participant Host as VoiceOS notch
    participant W as Your widget

    Host->>W: voiceos:init { data, args?, theme, radius }
    W->>Host: voiceos:resize { height }
    W->>Host: voiceos:updateInput { key, value }
    Note over W,Host: confirmation widgets only
    W->>Host: voiceos:openUrl { url }
    Note over W,Host: no approve message exists — the<br/>floating button is the only way to confirm
```

Two widget surfaces speak slightly different dialects:

|                       | Result widget                     | Confirmation widget                                          |
| --------------------- | --------------------------------- | ------------------------------------------------------------ |
| Where                 | A `widget` block in a glance card | A `widget` block as a confirmation view                      |
| `init` payload        | `{ data, theme, radius }`         | `{ data, args, theme, radius }`                              |
| `voiceos:resize`      | ✅                                 | ✅                                                            |
| `voiceos:openUrl`     | ✅                                 | ✅                                                            |
| `voiceos:updateInput` | — ignored                         | ✅                                                            |
| Approve/confirm leg   | none                              | **none** — approval is only ever the VoiceOS floating button |

## Host → widget

Sent once, when the frame loads:

```js theme={null}
// result widget
{ type: "voiceos:init", data: <block.data>, theme: { mode: "dark" | "light" }, radius: 26 }

// confirmation widget
{ type: "voiceos:init", data: <block.data>, args: <pending tool arguments>, theme: { mode }, radius: 26 }
```

* `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](/integrations/widgets#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`

```js theme={null}
parent.postMessage({ type: "voiceos:resize", height: 236 }, "*");
```

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`

```js theme={null}
parent.postMessage({ type: "voiceos:openUrl", url: "https://example.com" }, "*");
```

**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)

```js theme={null}
parent.postMessage({ type: "voiceos:updateInput", key: "body", value: "Edited text" }, "*");
```

Stages an edit to a pending tool argument; staged edits ride the user's
approval and replace the agent's proposed values.

| Field   | Rules                                                                                                                                                               |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `key`   | String ≤ 256 chars. `"body"` and `"{{body}}"` both address the argument `body`; dotted paths reach nested fields. Must name a property of the tool's `inputSchema`. |
| `value` | String, number, or boolean; coerced to string; ≤ 16,384 chars.                                                                                                      |

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](/integrations/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).
