Gamebus Docs

Custom menu items

Add a compact external iframe inside the participant side menu.

Use this for compact widgets

A custom menu item renders an external page directly inside the participant menu. The child page controls its own height, receives input collections, and can ask GameBus to navigate or store activities.

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

The iframe is rendered in the side menu, not in the main content area.

Dynamic height

The child should report its pixel height so the parent can avoid internal scrollbars.

Fast loading

Keep network work and visual complexity lightweight because menus should open quickly.

For full-page experiences, use custom pages. For mission task completion, use custom tasks.

Communication flow

Message protocol

DirectionTypeMeaning
Child to parentIFRAME_READYThe child has mounted. data.height can resize the iframe.
Parent to childINPUT_COLLECTIONSData fetched from the menu item's configured input collections.
Child to parentNAVIGATEAsk the parent app to navigate to an internal path.
Child to parentACTIVITYStore an activity and show normal success feedback.
Child to parentSILENT_ACTIVITYStore 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.org

The 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: 156

Send 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 Engines

Use this data for display and lightweight decisions. Validate the shape before using nested fields.

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_COLLECTIONS to the resolved menu item URL origin.
  • The child should verify event.origin when it knows the GameBus origin.
  • NAVIGATE should 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

ProblemCheck
Loader stays visibleThe child did not send IFRAME_READY, or it sent it from a different origin than the iframe URL.
Widget is clippedSend data.height as a positive number and update it after content changes.
Widget has a white boxSet the child page body background to transparent.
Input data is missingSend IFRAME_READY after registering the message listener, and check menu item input collections.
Navigation does nothingSend an internal path such as /activities.
Activity is rejectedCheck 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.

On this page