cue

Embedding a deck

Updated 2026-09-30

Any public cue deck can be placed in a web page — an article, a blog post, a product page — with one iframe. The deck plays in place, with no cue header and no sign-in. The page that frames it can also drive it: next, previous, go to a slide, switch language, and follow where the reader is.

The snippet

The deck’s owner copies this from the deck’s Share panel (it appears once the deck is public). The id is the one in the deck’s /view/<id> link.

<iframe src="https://cue.humanmeetsai.com/embed/YOUR_DECK_ID" width="100%" style="aspect-ratio:16/9;border:0" allowfullscreen loading="lazy" title="cue deck"></iframe>

The frame is 16:9 and as wide as its column. Making the deck private, or unpublishing it, stops every embed of it at once: the address then answers 404.

URL parameters

Optional, added to the embed address — for example /embed/YOUR_DECK_ID?slide=2&nav=1.

ParameterEffect
slide=<n>Open on slide n, counted from 1 (slide=2 is the second slide), 1–9999 — the same number ?slide carries on a /view address.0, or anything that is not a number in that range, opens on the first slide. The deck clamps a number past its end.
lang=<code>Start in that language, if the deck declares it (for example lang=de); otherwise ignored. A deck with more than one language also shows a small language picker in the frame.
nav=1Show the deck’s own on-canvas navigation controls. They are hidden by default.
autoplayAdvance by itself: autoplay or autoplay=1 every 6 seconds, autoplay=<s> every s seconds (4–60; other values turn it off). It returns to the start after the last step, pauses for 20 seconds whenever the reader (or your page) takes control, stops while the tab is hidden, and never runs when the reader asks for reduced motion.

Driving the deck from your page

Your page and the embed talk with postMessage. Every message is a plain object with a type. Slides and steps are counted from 0 and are absolute positions in the deck — the same numbering as ?slide.

Your page → the embed, sent as iframe.contentWindow.postMessage(msg, "https://cue.humanmeetsai.com"):

MessageEffect
{ type: "cue:hello" }Subscribes your page to the deck’s state, and is answered at once with the current state. Does not navigate.
{ type: "cue:goto", slide, step? }Go to a slide, and optionally a build step within it. slide is an integer 0–9999, step an integer 0–999 (default 0). The deck clamps a position past its end.
{ type: "cue:next" }One step forward, as the reader’s right-arrow key does.
{ type: "cue:prev" }One step back, as the reader’s left-arrow key does.
{ type: "cue:lang", lang }Switch the deck’s language. lang must be one of the languages the deck declares; anything else is ignored.

The embed → your page:

MessageWhen
{ type: "cue:ready", v: 1 }Once, when the embed is ready to receive messages. It carries nothing else.
{ type: "cue:state", v: 1, slide, step, slideCount, stepCount, lang }Only after your page has sent a valid message: at once as the answer (as soon as the deck has loaded, if it has not yet), then on every change, at most once per 100 ms (the latest state wins). All numbers are integers; lang is one of the deck’s declared languages, or null. It is sent to your page’s own origin only.

Handling messages safely

Any window can post a message to your page, so check both where a message came from and which window sent it before you act on it: e.origin === "https://cue.humanmeetsai.com" and e.source === iframe.contentWindow. When you post, always pass "https://cue.humanmeetsai.com" as the target origin, never "*". Treat what a cue:state carries as display data.

Attach your listener before the frame can load, as the example below does, and wait for cue:ready before you post. If your listener may be attached late — a script that loads after the page — it has missed cue:ready: post cue:hello when it attaches and repeat it until a cue:state answers. A hello moves nothing, does not pause ?autoplay, and is always answered.

Complete example

A deck with previous and next buttons, a “slide n / N” counter and a language switch. Replace YOUR_DECK_ID with your deck’s id, and the options with the languages your deck declares.

<iframe id="deck" src="https://cue.humanmeetsai.com/embed/YOUR_DECK_ID"
        style="width:100%;aspect-ratio:16/9;border:0"
        allowfullscreen title="cue deck"></iframe>
<p>
  <button id="prev" disabled>Previous</button>
  <span id="counter">Loading…</span>
  <button id="next" disabled>Next</button>
  <select id="lang" disabled>
    <!-- The languages your deck declares. -->
    <option value="en">English</option>
    <option value="de">Deutsch</option>
  </select>
</p>
<script>
  const APP = "https://cue.humanmeetsai.com";
  const frame = document.getElementById("deck");
  const el = (id) => document.getElementById(id);
  const send = (msg) => frame.contentWindow.postMessage(msg, APP);

  addEventListener("message", (e) => {
    // Trust only the embed itself: right origin AND right window.
    if (e.origin !== APP || e.source !== frame.contentWindow) return;
    const m = e.data;
    if (m?.type === "cue:ready") send({ type: "cue:hello" });
    if (m?.type === "cue:state") {
      el("counter").textContent = `slide ${m.slide + 1} / ${m.slideCount}`;
      if (m.lang) el("lang").value = m.lang;
      for (const id of ["prev", "next", "lang"]) el(id).disabled = false;
    }
  });
  // Ask again once the frame has loaded; harmless if cue:ready already did.
  frame.addEventListener("load", () => send({ type: "cue:hello" }));

  el("prev").onclick = () => send({ type: "cue:prev" });
  el("next").onclick = () => send({ type: "cue:next" });
  el("lang").onchange = (e) => send({ type: "cue:lang", lang: e.target.value });
</script>

Rules and limits

  • Public decks only. The embed address answers 404 for any other deck, and so has no API.
  • Only messages from the window that directly frames the embed count; a page nested deeper, or beside it, is not heard.
  • A message that is invalid — a wrong type of value, an out-of-range number — is ignored whole, and does not subscribe your page.
  • Unknown message types are ignored, in both directions. Ignore the ones you do not know.
  • A command sent before the deck has loaded is held and applied once it has (the latest navigation only).
  • A command from your page (every message but cue:hello) pauses ?autoplay for 20 seconds, as a reader’s interaction does.

Privacy

The embed tells your page the reader’s position and language only after your page asked for them, and only while it frames the deck. It sends them whether or not the reader has Do Not Track or Global Privacy Control on: the deck is public, your page asked, and it could observe what happens on its own page anyway. The embed never asks the reader to sign in. What cue records about an embed view is in the privacy policy.

Versioning

Messages from the embed carry v. It changes only when a message’s shape changes in a way that would break a page reading it. New message types can appear without a change of v — which is why unknown types must be ignored.

Questions

Write to info@humanmeetsai.com, or see support.

← Back to cue