doohnut-widget-sdk
TypeScript SDK for building Doohnut DOOH widgets.
Developer hub: developers.doohnut.com — Widgets / SDK (English) and Portuguese hub.
Doohnut widgets are HTML/JS bundles rendered inside iframes by the Doohnut Player. This SDK wraps the postMessage protocol between your widget and the player, giving you typed events and a clean API instead of raw message handling.
Installation
npm install doohnut-widget-sdk
# or
pnpm add doohnut-widget-sdkQuick start
import { DoohnutWidget } from 'doohnut-widget-sdk'
const widget = new DoohnutWidget({
readyPayload: { name: 'my-widget', version: '1.0.0' },
})
widget.on('init', ({ config }) => {
// config is the merged result from all config tiers
document.getElementById('title')!.textContent = String(config.title ?? 'Hello')
})
widget.on('configUpdate', ({ config }) => {
// hot-update your widget when config changes at runtime
document.getElementById('title')!.textContent = String(config.title ?? 'Hello')
})
widget.on('frameEnter', () => {
// frame became visible — start animations, resume video, etc.
})
widget.on('frameExit', () => {
// frame left viewport — pause, clean up resources
})Local development (Vite)
For Vite projects, the monorepo provides vite-plugin-doohnut-widget: a dev overlay at / with a mock host (INIT, FRAME_, CONFIG_UPDATE, STATE_, TIME_TICK), iframe URL under /__widget-app__/, package.json doohnut validation (including optional doohnut.config.fields shape checks), optional doohnut.devOverlay.presets and viewportPresets, a Viewport panel to resize the mock iframe, a visible mock STATE_* store, and optional DOOHNUT_PACK=1 tarball output (cross-env on Windows). See packages/vite-plugin-doohnut-widget/README.md and docs/widgets/development.md.
Layout and dimensions
Set doohnut.width and doohnut.height in package.json as the reference size (previews, thumbnails, and — unless you opt into responsive mode — the player’s fixed design canvas).
Set doohnut.aspectRatioPolicy:
mandatory(default when omitted) — the player lays out the iframe at your reference size and scales with contain inside the section. Use a fixed design plus inner scale-to-fit if needed.optional— responsive: the iframe fills the section; use fluid CSS (width/height100%, flex/grid) so layout follows the real section size.
See docs/widgets/protocol.md and docs/widgets.md.
Constructor options
const widget = new DoohnutWidget(options)| Option | Type | Default | Description |
|---|---|---|---|
autoReady | boolean | true | Automatically send a READY message when the DOM is loaded. Set to false for manual control. |
readyPayload | { name?: string; version?: string } | — | Optional payload included in the automatic READY message. Used for logging in the player. |
Events
Subscribe with .on(event, handler). The returned function unsubscribes.
const unsub = widget.on('init', ({ config }) => {
// handle config
})
unsub() // remove listener| Event | Fires when | Handler signature |
|---|---|---|
init | Player sends initial config (on load) | ({ config }) => void |
configUpdate | Merged config changes at runtime | ({ config }) => void |
frameEnter | Frame section becomes visible | () => void |
frameExit | Frame section leaves viewport | () => void |
timeTick | Player clock tick (reserved) | () => void |
init payload
{
config: Record<string, unknown> // merged config from all tiers
}A frameEnter event will follow init as a separate event if the widget was already visible at handshake time. Always handle frameEnter independently — do not check for a special flag in init.
Methods
sendReady(payload?)
Tells the player your widget is initialized. The player re-sends INIT after receiving this.
Called automatically by the constructor when autoReady: true (the default). Only call manually when using autoReady: false.
widget.sendReady({ name: 'my-widget', version: '1.0.0' })sendError(message, code?)
Report an error to the player (logged server-side).
widget.sendError('Failed to load data', 'FETCH_ERROR')destroy()
Remove all event listeners and cancel pending state requests. Call when your widget unmounts.
widget.destroy()State API (experimental)
Note: The state API is not yet implemented on the host side. These methods will reject until host support is released. Expose them in your widget code now for forward compatibility.
// Read state
const value = await widget.getState('widget', 'counter')
// Write state
await widget.setState('widget', 'counter', 42)
// Delete a key
await widget.clearState('widget', 'counter')
// Delete an entire scope
await widget.clearState('widget')State scopes
| Scope | Description |
|---|---|
widget | Per-widget, per-display. Resets when the widget reloads. |
playlist | Shared across all widgets in the same playlist. |
instance | Deprecated. Reserved for protocol compatibility; not used by the current player. |
campaign | Shared across all playlists in the same campaign. |
Config tiers
The config object you receive in init and configUpdate is already merged. The player resolves config from four tiers (lowest → highest priority):
| Tier | Set by |
|---|---|
Widget defaults (package.json) | Widget author |
| Campaign config | Campaign editor |
| Playlist config | Playlist editor |
| Frame/facet config | Frame editor |
Higher-tier values override lower-tier values for the same key. Your widget always receives the final merged result — you don't need to handle merging.
TypeScript
All types are bundled — no separate @types/ package needed.
import type {
IWidgetConfigUpdatePayload,
IWidgetInitPayload,
TWidgetStateScope,
WidgetEventMap,
} from 'doohnut-widget-sdk'Without the SDK
If you prefer raw postMessage handling, the protocol constants are also exported:
import {
createWidgetToHostMessage,
isWidgetProtocolMessage,
WIDGET_MSG_CONFIG_UPDATE,
WIDGET_MSG_FRAME_ENTER,
WIDGET_MSG_FRAME_EXIT,
WIDGET_MSG_INIT,
WIDGET_MSG_READY,
WIDGET_PROTOCOL_SOURCE_HOST,
} from 'doohnut-widget-sdk'
window.addEventListener('message', (e) => {
if (!isWidgetProtocolMessage(e.data)) { return }
if (e.data.source !== WIDGET_PROTOCOL_SOURCE_HOST) { return }
switch (e.data.type) {
case WIDGET_MSG_INIT: {
const { config } = e.data.payload as { config: Record<string, unknown> }
applyConfig(config)
break
}
case WIDGET_MSG_FRAME_ENTER:
// widget is visible — start animations, resume video, etc.
break
case WIDGET_MSG_FRAME_EXIT:
// widget left viewport — pause, release resources
break
case WIDGET_MSG_CONFIG_UPDATE: {
const { config } = e.data.payload as { config: Record<string, unknown> }
applyConfig(config)
break
}
}
})
// Pull model: send READY so the host responds with INIT then FRAME_ENTER (if already visible).
window.parent.postMessage(createWidgetToHostMessage(WIDGET_MSG_READY, { name: 'my-widget' }), '*')For the full wire-level protocol specification see docs/widgets/protocol.md.
License
MIT