Integration guide

From a checkout to a working embed.

Build the files, put them on your site with the demo apartment from this site as the scene, then swap in your own scene and pick the integration path that suits your site. The README holds the full reference; this page is the route to the first embed.

Step 01

Get the files.

The player is not published to a package registry; you build it from source. You need Node 22.12 or newer and Bun.

terminal
git clone https://github.com/jeex-org/jeex-viewer.git
cd jeex-viewer
bun install --frozen-lockfile
bun run build:lib    # dist-lib/: the library, the custom element and the public embed.html

The build ends with a check of the shipped stylesheet. dist-lib/ is what you put on your site; the ES entries import hashed sibling chunks, so copy the whole directory, never a single file.

Step 02

Try it with the demo apartment.

The quickest proof that the files work on your site: copy the whole dist-lib/ directory to /jeex-viewer/ on your server and paste the element into any page. The scene is the player's own demo apartment, served by this site with CORS so any origin can load it.

any-page.html
<!-- /jeex-viewer/ is your site's copy of dist-lib/ -->
<script type="module" src="/jeex-viewer/jeex-viewer-element.js"></script>
<!-- the demo apartment, for trying the player; your own scene goes here -->
<jeex-viewer src="https://player.jeex.org/demo/demo-apartment.json" lang="en"></jeex-viewer>

This is the demo, not your property. The demo apartment is there for trying the player; for your property you make your own apartment's scene, and the next step says what it has to be.

Step 03

Supply your own scene.

The player renders a scene document (schema 2.1: walls, openings, rooms, furniture, finishes) or a baked project folder whose root is a project.json. It does not create scenes. The URL you give it must hold to these rules:

  • CORS from wherever it is served — the fetch comes from the viewer's origin, without credentials
  • A Content-Type that includes application/json
  • At most 8 MB, no redirects, an answer within 15 seconds
  • Every id in the document is a UUID v7 — the viewer validates ids and never mints one

A JSON Schema generated from the same type is in dist-lib/schema/scene.schema.json. A document the validator refuses fails with the code scene-invalid and a message that names the failing rule.

Step 04

Choose an integration method.

Serve the whole dist-lib/ directory from one origin: the ES entries import hashed sibling chunks, so a single file is not enough, and every URL inside it is relative. Below, /jeex-viewer/ is where your site serves that directory, and the scene is the demo apartment: put your own scene's URL in its place.

Custom element, ES module

Modern browsers, the lightest download. Plain HTML, nothing a CMS block has to allow beyond the tags.

<!-- /jeex-viewer/ is your site's copy of dist-lib/ -->
<script type="module" src="/jeex-viewer/jeex-viewer-element.js"></script>
<!-- the demo apartment, for trying the player; your own scene goes here -->
<jeex-viewer src="https://player.jeex.org/demo/demo-apartment.json" lang="en"></jeex-viewer>

Custom element, classic script

For a CMS that strips type="module": jeex-viewer.iife.js registers the same element on load.

<!-- /jeex-viewer/ is your site's copy of dist-lib/ -->
<script src="/jeex-viewer/jeex-viewer.iife.js" defer></script>
<!-- the demo apartment, for trying the player; your own scene goes here -->
<jeex-viewer src="https://player.jeex.org/demo/demo-apartment.json" lang="en"></jeex-viewer>

iframe

Any page that allows an iframe. dist-lib/embed.html ships no scene, so it takes one in ?scene=. For isolation, serve it from an origin of its own (see Hosting below) and write that origin in place of /jeex-viewer/.

<!-- /jeex-viewer/ is your copy of dist-lib/; ?scene= is the demo apartment -->
<iframe src="/jeex-viewer/embed.html?scene=https%3A%2F%2Fplayer.jeex.org%2Fdemo%2Fdemo-apartment.json&lang=en"
  title="Apartment tour" loading="lazy" allow="fullscreen"
  style="display:block;width:100%;aspect-ratio:16/9;border:0"></iframe>

To drive the frame from your page (play, seek, switch modes, listen to events), let the small host helper create it: embedViewer from jeex-viewer-host.js checks the origin of every message and queues commands until the frame is ready.

ES module

For a page that wants the handle API directly. The container needs a height; the viewer fills it and never takes the page over.

// /jeex-viewer/ is your site's copy of dist-lib/
import { mountViewer } from '/jeex-viewer/jeex-viewer.js';

const viewer = mountViewer(document.getElementById('apartment'), {
  // the demo apartment, for trying the player; your own scene goes here
  scene: { url: 'https://player.jeex.org/demo/demo-apartment.json' },
  view: { mode: 'walk', minimap: true },
  tour: { mode: 'paused', progressBar: false },
});

await viewer.ready;
viewer.tour.play();

mountViewer is the authoring API: unlike the element and the frame it is not read-only by default.

Step 05

Size and configure.

  • A bare <jeex-viewer> is a block at its parent's full width and 16:9 of it — ratio="4:3" changes that, and any width, height or aspect ratio your page sets wins
  • The iframe's box is its inline aspect-ratio — embed.html fills whatever box its frame has
  • The element and the frame are read-only for visitors — editing panels need an explicit readonly="0"

Every option has one name in each spelling: an object key for mountViewer, an attribute on the element, a query parameter on embed.html. For example, a paused tour with the mini-map and a pinned control bar:

<!-- the demo apartment, for trying the player; your own scene goes here -->
<jeex-viewer src="https://player.jeex.org/demo/demo-apartment.json"
  view="tour" tour="paused" minimap="1" chrome-auto-hide="0" lang="en"></jeex-viewer>

An unknown key is ignored; an invalid value falls back to the default and is reported once with the code option-invalid.

Step 06

Hosting, CSP and troubleshooting.

The viewer runs under a strict Content Security Policy with no unsafe-inline of any kind:

default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'
  • connect-src must cover wherever the scene is fetched from
  • The iframe path also needs frame-src for the viewer's origin
  • Serve embed.html from an origin of its own — and list the sites allowed to frame it in a Content-Security-Policy: frame-ancestors response header

When something does not show

  • An error card that says no scene source — the element has no src, or dist-lib/embed.html has no ?scene=
  • The scene does not load — check CORS, the Content-Type and the 8 MB cap; the error names the status and the URL
  • Nothing visible with mountViewer — the container has no height, and 100% of an unsized parent is zero
  • Fullscreen reports it is unavailable — the frame lacks allow="fullscreen"
  • A theme variable has no effect — set it on the .jeex-viewer root itself, not on an ancestor
  • No WebGL context — the viewer shows its fallback card and raises webgl-unavailable

Step 07

Full reference.

Everything past the first embed lives in the README.