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.
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.
<!-- /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-Typethat includesapplication/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.htmlfills 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-srcmust cover wherever the scene is fetched from- The iframe path also needs
frame-srcfor the viewer's origin - Serve
embed.htmlfrom an origin of its own — and list the sites allowed to frame it in aContent-Security-Policy: frame-ancestorsresponse header
When something does not show
- An error card that says no scene source — the element has no
src, ordist-lib/embed.htmlhas no?scene= - The scene does not load — check CORS, the
Content-Typeand 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-viewerroot 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.
- Optionsevery option in its three spellings
- Viewer handle APIplayback, modes, selection, captures
- Eventshandle and element events
- Errorscodes to branch on
- iframe with postMessagethe host helper and the wire protocol
- Scene sourcesURL, document, repository
- Scene documentschema 2.1 and its rules
- Project formatthe baked project folder
- Themingthe ten custom properties and the parts
- Languagescatalogues and your own locale
- Accessibilitykeyboard, screen readers, motion
- Content Security Policywhat a strict page needs
- iframe permissions and originsandbox, title, framing
- install.mdthis route as an instruction for an AI agent