Skip to main content
The Widget Kit is a single dependency-free module — widgetKit.ts — that turns structured content into polished widget HTML: correct theming, brand accents with contrast repair, your integration’s mark on every card, and the whole bridge runtime implemented for you. It’s the exact system the Integration Studio uses, which is why its output looks native in the notch. Getting it: every Studio-generated folder ships with its own copy, with your logo baked in. Building by hand, copy widgetKit.ts from a generated folder — or src/widget/kit.ts from the SDK — next to your server.ts.

Composed cards: renderWidget

Build blocks, render, return:
renderWidget returns { html, height, label } — and the height is real: every component reports its measured height, so the declared estimate lands within a few percent of the rendered card.

The components

Each returns a Block for the blocks array: Tones are neutral, good, bad, accent. Helpers: esc() (escape any interpolated string — always), clip(value, max), and textEms() for CJK-aware width estimates.

Accent and theme

Never use prefers-color-scheme. It follows the OS while the notch stays dark — a light-mode Mac would render a light card on black glass.
The surface finish is drawn for you. Every kit card carries a liquid-glass rim — a 1px inset hairline, brighter along the top, on the same --k-radius arc the host clips at, so it can neither gap nor get shaved. Declare an accent and the card also gets a soft brand wash out of the top-left corner. The integration mark stays the bare logo; the kit never puts a tile or colored backdrop behind it, and neither should you.
Don’t add your own outer border, glow, or logo tile. The finish that belongs on the card is already there.

Bespoke layouts: renderCustom

When the composed blocks can’t express your card, own the surface:
Same shell — variables, bridge runtime, mark — with zero layout opinions. Two differences from renderWidget:
  • You own the padding and the declared height.
  • It throws if the document exceeds the byte cap instead of degrading — an error during your test run beats a card the host silently drops.
If your body forgets markHtml(), the kit floats the mark over the top-left anyway. No card ships unbranded.

Images and fonts

The sandbox has no network, so fetch assets in the handler and inline them:
Budgets: ~32 KB per image, ~42 KB for one pre-subsetted WOFF2 font, and half the total HTML budget across all images. Over-budget assets come back null — design the card to survive a missing image. The kit’s runtime wires interactivity declaratively — you write no bridge code:
  • Fields. vField({ key: "body", label: "Message" }) renders an input bound to the tool argument body. In confirmation cards it hydrates from the pending call’s args and stages the user’s edits. In custom markup, any element with data-voiceos-key="body" gets the same treatment.
  • Links. vList rows with href, elements with data-k-link, and plain <a href="https://…"> anchors all route through the bridge to the user’s browser. https only.

Staying under the caps

The HTML budget is 128 KB. renderWidget degrades gracefully when a card exceeds it — content images are stripped first (never your mark), then trailing blocks — so a card never silently vanishes. Confirmation-mode cards (mode: "confirm") also reserve the bottom-right corner for VoiceOS’s floating confirm button.

The design bar

The same rules the Studio’s design critic scores against — worth keeping even when you’re hand-rolling:
  • At most 3 type sizes; tabular-nums on every number.
  • Truncate, never wrap — and text-overflow: ellipsis needs overflow: hidden to work.
  • Hairlines and spacing over nested boxes; spend one accent deliberately.
  • Fit 420px without scrolling — show 4–5 rows and a “+N more”, not a scrollbar.
  • Everything renders offline, on first paint, in both themes.

Widget confirmations

The same kit, one extra rule: VoiceOS owns the confirm button.