Skip to main content
Two questions decide how a demo gets in front of someone. Does it run as its own page or inside your React app? And for the page, who hosts it?

Which do you want?

For the page, the second question is who hosts it: The snippets below are the same either way. Only the origin in them differs. Publish and self-host covers both routes. The top row is the same page used three ways. Send someone the URL and they get the demo full-screen. Frame that URL and it sits in your page. Point the loader script at it and it opens over your page. Nothing is installed, and the embedding page can be React, Vue, Rails, or plain HTML. The bottom row skips the page. You install the runtime package and render the player inside your own React tree, so there is no iframe and no second document. Pick the top row if you want isolation, or if the demo lives on a different host from the site showing it. Pick the bottom row if you want the demo to behave like part of your app, with your router, your state, and your styling around it. An iframe costs you a second document and cross-document messaging for size and events. The component costs you the player in your bundle.
The two rows need different things deployed. The top row needs the page at a URL: published, or dist/ from build on your host. The bottom row does not use dist/ at all. It needs the demo folder (demo.config.json and assets/) reachable as static files, plus @inkly-org/interactive-demo installed.
The demo page is complete on its own. The simplest thing you can do with a demo is send someone its URL: the one publish printed, or dist/<slug>/ on your host. It is the same URL the iframe and the pop-up point at. Add ?autoplay=1 to start playing as soon as it loads.

Inline: the iframe

Add ?embed=inline to the page URL and the page renders the player alone: no bar, no canvas, a transparent background, and the player filling the frame edge to edge. Size the frame to the demo. That is its screen ratio, plus the player’s header (52px, or 0 when the demo sets chrome.hideHeader), plus 2px for the card’s border:
You do not have to work the numbers out. interactive-demo embed prints this snippet with the numbers filled in from the demo. So do publish, build (with a placeholder host), and the editor’s Share dialog. The outer max-width keeps the frame under 80% of the viewport height. The inner box is the exact ratio plus header, so the player never letterboxes.
  • allow="fullscreen" lets the player’s fullscreen button work inside the frame.
  • loading="lazy" keeps the player off the critical path of the host page.
  • Below about 640px wide, the player switches to its mobile layout.
  • Without ?embed=inline the page keeps its own bar and canvas. That is what you want for a link, not an embed.
The player never reads or writes anything outside its own document. It makes no network requests beyond loading its own files and assets. The one exception is a form widget with a submitTo URL, which POSTs its values there.

Pop-up: the loader script

To open a demo from a button, include the loader once and call InteractiveDemo.open with the demo’s URL. The loader is three kilobytes with no dependencies. build writes it to dist/embed.js, and the hosting service serves the same file at /embed.js.
The first line is a stub that queues clicks made before the script arrives. open draws a scrim and a centred frame and loads the page inside it with ?embed=inline. It closes on Escape, on a click outside the frame, or on InteractiveDemo.close(). The frame starts at 16:9 and takes the demo’s own ratio as soon as the page reports its size. A relative URL is resolved against the host page. interactive-demo embed --mode popup prints the loader and a button for HTML, React, Next.js, Vue, and Svelte.

Listen for events from an embedded demo

A framed demo, inline or in the pop-up, relays every runtime event to the page that embeds it as a message of type interactive-demo:event. Form submissions, step views, completion, and button clicks all arrive this way, so the host page can react without touching the demo:
Check e.origin against the host you embed from before trusting the payload. A demo that stands on its own page sends nothing. The React component gets the same events through its onEvent prop. The event list is in the runtime reference. A form can also POST its values to a URL of your choosing with submitTo.

In your React app

If the host page is React, skip the iframe and render the player inline. Copy the demo’s source folder (demos/<slug>/, not the built one) into your app’s static files, such as public/ in Next.js or Vite, and point the player at it:
The component fetches demo.config.json from that folder and resolves the media paths in it against the same folder. To import the config at build time instead, pass the object and say where its folder is served from:
Media served from somewhere else, such as a CDN, goes through resolveAssetUrl={(path) => …}, which receives each relative path. The component takes the same config the static page embeds, so a demo authored in the editor works in both places without changes.

A pop-up in React

Wrap the player in DemoModal rather than loading embed.js. It renders the player in-process through a portal with the same overlay, so there is no iframe and no second copy of the runtime.
DemoModal mounts its children only while open, so the demo starts from the beginning each time. It hands focus back to the button on close. Every prop is in the runtime reference.