Custom menu items
Add a compact external iframe inside the participant side menu.
Use this for compact widgets
Custom menu items are best for small widgets: progress summaries, shortcuts, reminders, compact dashboards, or campaign-specific actions that should stay visible in the menu.
Menu item access, slots, ordering, and expression resolution are explained in Campaigns: menu items. This page focuses on the external iframe protocol.
Design constraints
Small surface
Dynamic height
Fast loading
For full-page experiences, use custom pages. For mission task completion, use custom tasks.
Communication flow
Message protocol
| Direction | Type | Meaning |
|---|---|---|
| Child to parent | IFRAME_READY | The child has mounted. data.height can resize the iframe. |
| Parent to child | INPUT_COLLECTIONS | Data fetched from the menu item's configured input collections. |
| Child to parent | NAVIGATE | Ask the parent app to navigate to an internal path. |
| Child to parent | ACTIVITY | Store an activity and show normal success feedback. |
| Child to parent | SILENT_ACTIVITY | Store an activity quietly. |
Configure the menu item
A custom menu item needs a URL that points to the child page. It can also define input collections and activity templates.
title: Progress
url: https://widgets.example.org/progress-menu-item
slot: center
order: 20
inputCollections:
- key: participant
inputRequests:
- key: progress
endpoint: /api/embed/progress
activityTemplates:
- reference: progress-widget-clicked
embeddedActivityProviders:
- origin: https://widgets.example.orgThe title and url fields may contain expressions that use resolved input collections. See Campaigns: menu items for the resolution rules.
Build the child page
The child can be a public GameBus route or an external page.
Minimal Svelte example:
<script lang="ts">
import { onMount } from 'svelte';
type ParentMessage = { type: 'INPUT_COLLECTIONS'; data?: Record<string, Record<string, unknown>> };
let inputCollections: Record<string, Record<string, unknown>> = $state({});
function getDocumentHeight() {
return Math.ceil(
Math.max(
document.body.scrollHeight,
document.body.offsetHeight,
document.documentElement.scrollHeight,
document.documentElement.offsetHeight
)
);
}
function postReady() {
window.parent.postMessage(
{ type: 'IFRAME_READY', data: { height: getDocumentHeight() } },
'*'
);
}
onMount(() => {
postReady();
const observer = new ResizeObserver(() => postReady());
observer.observe(document.body);
observer.observe(document.documentElement);
return () => observer.disconnect();
});
function handleMessage(event: MessageEvent<ParentMessage>) {
if (!event.data || typeof event.data !== 'object') return;
if (event.data.type === 'INPUT_COLLECTIONS') {
inputCollections = event.data.data ?? {};
queueMicrotask(postReady);
}
}
function openActivities() {
window.parent.postMessage({ type: 'NAVIGATE', data: '/activities' }, '*');
}
</script>
<svelte:window onmessage={handleMessage} />
<section class="card">
<strong>Campaign progress</strong>
<pre>{JSON.stringify(inputCollections, null, 2)}</pre>
<button type="button" onclick={openActivities}>View activities</button>
</section>
<style>
:global(body) {
background: transparent;
margin: 0;
}
.card {
box-sizing: border-box;
width: 100%;
padding: 0.75rem;
}
</style>Report iframe height
The parent starts with a fallback height, then updates the iframe when the child sends IFRAME_READY with a positive finite number.
type: IFRAME_READY
data:
height: 156Send pixels as a number, not a CSS string. These values are valid:
{ type: 'IFRAME_READY', data: { height: 156 } }
{ type: 'IFRAME_READY', data: { height: Math.ceil(document.body.scrollHeight) } }These are ignored:
{ type: 'IFRAME_READY', data: { height: '156px' } }
{ type: 'IFRAME_READY', data: { height: 0 } }
{ type: 'IFRAME_READY', data: { height: -1 } }Use ResizeObserver because the menu item can change height after input data arrives, fonts load, or messages appear.
Receive input collections
GameBus sends input collections after the child sends IFRAME_READY.
type: INPUT_COLLECTIONS
data:
participant:
progress:
completedTasks: 7
totalTasks: 10
team: Analytical EnginesUse this data for display and lightweight decisions. Validate the shape before using nested fields.
Navigate the parent app
Use NAVIGATE for shortcuts from the widget into GameBus.
window.parent.postMessage(
{
type: "NAVIGATE",
data: "/activities",
},
"*",
);The parent accepts internal same-origin paths only.
Store an activity from the widget
Menu item embeds can also store activities when the menu item allows the iframe origin for the selected template.
window.parent.postMessage(
{
type: "ACTIVITY",
data: {
template: "progress-widget-clicked",
start: new Date(),
end: new Date(),
properties: [
{
template: "action",
obj: { value: "view-activities-clicked" },
},
],
},
},
"*",
);For background events that should not show normal success feedback, send SILENT_ACTIVITY with the same data shape.
Security checklist
- The parent accepts messages only from the resolved menu item URL origin.
- The parent posts
INPUT_COLLECTIONSto the resolved menu item URL origin. - The child should verify
event.originwhen it knows the GameBus origin. NAVIGATEshould contain internal GameBus paths only.- Activity templates must be allowed for the menu item and iframe origin.
- Keep the widget resilient if input collections are missing or partial.
Visual checklist
- Keep text short and scannable.
- Avoid fixed desktop widths.
- Use transparent backgrounds when the GameBus menu surface should show through.
- Avoid internal scrollbars; send the correct height instead.
- Recalculate height after any content change.
- Keep network requests lightweight.
- Make buttons and links readable at side-menu width.
Troubleshooting
| Problem | Check |
|---|---|
| Loader stays visible | The child did not send IFRAME_READY, or it sent it from a different origin than the iframe URL. |
| Widget is clipped | Send data.height as a positive number and update it after content changes. |
| Widget has a white box | Set the child page body background to transparent. |
| Input data is missing | Send IFRAME_READY after registering the message listener, and check menu item input collections. |
| Navigation does nothing | Send an internal path such as /activities. |
| Activity is rejected | Check activity template reference and allowed embedded activity provider origin. |
Summary
To add a custom menu item, configure a participant menu item with a trusted iframe URL, build a compact child page, send IFRAME_READY with measured height, receive INPUT_COLLECTIONS, then optionally post NAVIGATE, ACTIVITY, or SILENT_ACTIVITY messages back to GameBus.