The embed element
<rendemo-demo>: two lines to install a published Rendemo demo. Every attribute, the five DOM events, framework wrappers, and the limits that apply today.
Two lines
One script tag and one element. No package, no build step, no version to pin.
<script src="https://www.rendemo.com/embed.js" async></script>
<rendemo-demo demo="<demo-id | workspace/slug>"></rendemo-demo>Both demo forms resolve to a real, publicly-servable, framable route: a UUID resolves to /d/<id>, and workspace/slug resolves to /site/<workspace>/<slug>. Either form works inside the <iframe> the element creates on your site.
/embed.js is one unversioned URL. A fix reaches a given visitor somewhere between immediately and roughly a day out — a visitor’s own browser treats an already-fetched copy as fresh for up to 5 minutes, and the CDN in front of it for up to an hour, beyond which it may keep serving a stale copy for another 24 hours while revalidating. Nothing on your side has to be bumped.Attributes
Three attributes are observed — demo, open and user — meaning changing them after mount takes effect. Everything else is read once, at connect.
| Attribute | Values | Notes |
|---|---|---|
| demo | demo UUID, or workspace/slug | Required. Reactive — changing it after mount re-resolves: a live iframe’s src is swapped in place, or (if not yet started or opened) the still-showing poster <img> is repointed at the new demo’s still. If the new value does not resolve to anything servable and an iframe is already mounted, that iframe is simply removed — no fallback message is shown in its place. |
| mode | inline (default) · modal · tour | Read once. tour runs guidance on your own product instead of a replay: no iframe, no poster, no box (display:contents), and the step card is drawn in its own fixed layer — see tours. guide is accepted as a silent alias for tour. |
| open | present / absent | Modal only — the host controls it. Reactive: toggling it after mount opens and closes the modal live. |
| poster | auto (default) · off | Read once. See poster and ratio. |
| ratio | 16:10 (default) · auto | Read once, except that the poster’s load handler re-reads it once the image arrives. 16:10 (or any W:H) sets a fixed aspect ratio immediately; auto defers to the poster image’s intrinsic size. |
| locale | BCP-47 | Read once, and re-read automatically whenever demo changes — both feed the same iframe-src resolution. Forwarded to the demo as a query param. |
| user | any opaque id | Tour only, optional. Keys progress by this id instead of by browser, and rides on the analytics events, so completion is per person. Reactive. Ignored in inline/modal, where re-resolving would reload the iframe. Rendemo never resolves it to anybody; bounded at 64 characters. |
| routes | comma-separated paths | Tour only, optional. The pages this tour may run on at all. Read at mount, re-evaluated on every navigation. Absent means every page carrying the element. |
| when | once (default) · always | Tour only, optional. always replays a tour the viewer already finished or dismissed while still resuming a run in progress. An unrecognised value is once. |
Poster and ratio
poster="auto" (the default) shows a static still while the iframe is deferred — the element costs no iframe and no JS runtime beyond embed.js until someone clicks to start the demo.
- Source. A JPEG written to storage at publish time, from the capture’s first usable keyframe screenshot. It is generated best-effort — a failed screenshot never fails the publish. A demo without a usable frame simply has no poster: the
<img>404s, the element removes it onerror, and the start button falls back to its own gradient background. - Access. Gated identically to the demo it depicts — published, not password-protected, not expired. It never leaks a still of a protected or expired demo.
ratio="auto"depends on it. The poster’s intrinsic width and height, once loaded, set the host’saspect-ratioin inline mode — no extra request, since the poster was already fetched. Combiningposter="off"withratio="auto"inline leaves nothing to derive a size from, so the element falls back to an explicitaspect-ratio: 1.6(16:10) rather than no size at all. In modal mode the panel always gets a numeric ratio, falling back to the same 1.6.
Load timeout
Inline mode only: once the visitor clicks the start button, the element waits up to 9 seconds for the framed demo to post ready. If it does not arrive — a deleted, expired or unpublished demo, or just a slow load — the element replaces its content with the same “This interactive demo could not be loaded” / “Open it in a new tab” fallback an unresolvable demo attribute gets, instead of leaving the visitor at “Preparing the interactive demo” forever. Modal mode has no equivalent timeout: opening the modal mounts the iframe directly, with nothing bounding how long it waits for ready.
The five events
The element listens for messages from the framed demo (origin- and source-checked) and re-emits the matching ones as DOM CustomEvents. Five names, and only these five — this is the whole contract surface, and it does not grow casually.
| Event | Fires when | detail |
|---|---|---|
| rendemo:ready | The framed demo has finished loading and rendered its first frame. | A passthrough of whatever internal player state accompanied the message at that moment. This is not a documented contract and can change shape without notice — treat rendemo:ready as a bare signal, not a source of fields to read. |
| rendemo:step | The visitor advances to a new step (once per step transition, not once per card re-render). | { id, n, total } — n is the 1-based step number the visitor is now on, total the step count for the whole demo. |
| rendemo:complete | The visitor reaches the end of the demo. | {} — no fields. |
| rendemo:lead | A lead-capture form is successfully submitted, either the in-card capture or the full-screen gate. | { where } — "card" or "gate". Deliberately no field values — no email, name, or anything else the visitor typed. That is the no-PII embed contract; look up the actual lead data server-side, keyed by your own tracking. |
| rendemo:close | Modal only. The visitor clicks the close button, clicks the backdrop, or presses Escape — including Escape pressed inside the framed demo. | {} — no fields. It is a request to close, not a notification that the modal already closed: the element does not close itself, and the host is expected to clear the open attribute in response. |
const demo = document.querySelector("rendemo-demo");
demo.addEventListener("rendemo:ready", () => analytics.track("demo_ready"));
demo.addEventListener("rendemo:step", (e) => {
console.log(`step ${e.detail.n}/${e.detail.total}: ${e.detail.id}`);
});
demo.addEventListener("rendemo:complete", () => {
// e.g. reveal a "Talk to sales" CTA once the visitor has seen the whole demo
document.querySelector("#post-demo-cta").hidden = false;
});
demo.addEventListener("rendemo:lead", (e) => {
analytics.track("demo_lead_captured", { where: e.detail.where });
});A tour dispatches the same four of these it can (rendemo:ready, rendemo:step, rendemo:complete, rendemo:close) with tour-shaped details — see running a tour. A tour renders no in-card form, so rendemo:lead never fires for one.
Framework wrappers
Ask the MCP. rendemo_get_embed({ projectId, framework, mode }) returns everything needed to install a published demo — it fails with a clear message if the project has no demo yet, or the demo is not published:
scriptTagandscriptLocation(where the tag goes for that framework),- the
snippetto drop where the demo should appear, wrappersource fornext-app-router/next-pages/react/vue/svelte— empty forhtml, since there is nothing to wrap,- the resolved
urlandposterUrl, andevents— the same five names above, sourced from one place so the docs and the tool cannot drift.
framework defaults to next-app-router; mode defaults to inline.
A wrapper may do exactly two things
Forward props to attributes, and bridge the five events above to callbacks. Anything else belongs inembed.js, which can be fixed for every site at once; a wrapper lives in your repo, where it cannot. Rendemo’s own landing page wrapper obeys the same rule — it is not special-cased.Modal mode
mode="modal" builds a real dialog, not just a styled <div>:
role="dialog",aria-modal="true",aria-label="Interactive product demo"on the dialog root.- Focus trap via two visually-hidden, tab-focusable sentinel nodes bracketing the panel — not
display:none, which would drop them from the tab order entirely. They catch focus leaving in either direction, including focus that walked out of the cross-document iframe, whose contents fire no event the parent could see. On open, focus lands on the close button, which is what keeps the very first Shift+Tab from escaping. - Escape closes from the host document and from inside the demo: a small bridge script injected into every served demo page forwards Escape keydowns to the parent.
- Scroll lock —
document.body.style.overflowis set tohiddenwhile open and restored to its prior value on close. - Focus restoration — whatever had focus before the modal opened is refocused when it closes.
The host owns open. Listen for rendemo:close and clear the attribute — the element deliberately does not close itself.
Limits worth knowing
A strict Content Security Policy must allow the script
A strictscript-src on the host page must allow https://www.rendemo.com, or the failure is loud: the element never upgrades and its fallback link renders instead of the demo.Password-protected demos cannot be embedded today
When the visitor submits the password, the framed page’s Set-Cookie for the access token is issued as SameSite=Lax with no Partitioned attribute and no SameSite=None. Inside the third-party iframe the embed creates, that is a third-party cookie, and browsers that enforce SameSite and partitioning defaults — most of them now — drop it on write. The password check succeeds, the redirect fires, the cookie never lands, and the visitor is back on the password form.
There is no workaround at the embed layer. Until the cookie change lands on those routes, do not embed password-protected demos — link to them directly (/d/<id> or /site/<workspace>/<slug> in a new tab), where the cookie is first-party and works normally.
The other two are stated where they belong: modal mode has no load timeout, and rendemo:ready’s detail is not a contract.