Docs
How it works.
Six plates, in install order. Written for the person who will actually paste the code.
Install the embed
Two lines. The script registers the <xyzski-mountain> element; the element renders your mountain. No build step, no framework requirement, no iframe.
<script async src="https://www.xyzski.com/embed/v1.js"></script>
<xyzski-mountain resort="sunlight" token="demo"></xyzski-mountain>Easiest route — any CMS editor can do this.
On load the element calls xyzski.com/api/embed/config with your resort and token; the response carries the terrain bundle URL and your license flags. Your token only works on the domains registered to your license — tell us every domain that will host the map (staging included) and we'll add them. The demo token works on xyzski.com and localhost only.
Attributes reference
Web-component configuration.
| Attribute | Type | Default | Does |
|---|---|---|---|
| resort | slug | required | Which mountain to render — assigned when we bake your bundle (e.g. sunlight). |
| token | string | required | Your license token. Validated against your registered domains; the special token demo works on xyzski.com and localhost only. |
| theme | preset | JSON | "mono" | Visual theme: the monochrome ink default, classic trail-map colors, or a JSON theme object (Ridgeline+). |
| interactive | boolean | true | Enables orbit and hover picking in the expanded view. Set false for a purely ambient hero. |
| expanded | boolean | false | Start in the full explorer view (labels, orbit, layer choreography) instead of the compact ambient view. |
Size the element with CSS like any block element — the canvas fills its box and resizes with it. The map-data attribution chip renders inside the component on every tier (it's a license obligation, not branding).
Events API
Summit tier — the full viewer stream.
Every tier gets xyzski:loaded and xyzski:error on the element. On Summit and Enterprise (and the demo token, so this sample runs as-is), the full viewer stream re-dispatches as xyzski:event and the element exposes el.viewer — the live viewer handle for layer toggles and expansion:
// Every tier: readiness and failure fire as DOM events on the element.
const el = document.querySelector('xyzski-mountain')
el.addEventListener('xyzski:loaded', ({ detail }) =>
console.log(detail.resort, 'ready —', detail.quality))
el.addEventListener('xyzski:error', ({ detail }) => console.warn(detail.message))
// Summit tier (and the demo token): the full viewer stream re-dispatches
// as xyzski:event — drive your own run panels, snow reports, webcams.
el.addEventListener('xyzski:event', ({ detail: e }) => {
if (e.type === 'select') openRunPanel(e.feature) // your UI
if (e.type === 'hover' && e.feature) showTooltip(e.feature.name)
})
// Summit tier also exposes el.viewer — the live viewer handle
// (null until xyzski:loaded fires).
el.addEventListener('xyzski:loaded', () => {
el.viewer?.setLayers({ lifts: false })
})| type | payload | fires when |
|---|---|---|
| loaded | { resort, quality, manifest } | Bundle parsed and first frame rendered. quality is 'full' or 'lite' (software-GPU fork). Also fires as the dedicated xyzski:loaded event on every tier. |
| hover | { key, feature } | Pointer enters/leaves a run, glade, or lift. feature carries name, difficulty, length, and drop; both fields are null on leave. |
| select | { key, feature } | Guest clicks a run. Drive your own UI: run detail panels, snow reports, webcams. |
| poi | { poi, screen, via } | A point of interest (lift terminal, lodge, summit) was clicked or tracked; screen gives canvas-relative pixel coordinates. |
| contextlost | { lost } | The browser dropped or restored the WebGL context. The embed posters itself and recovers — listen only if you render custom chrome. |
| error | { error } | The bundle failed to load. The embed shows its fallback state; log it if you want visibility. License/config failures fire the dedicated xyzski:error event ({ code, status, message }) on every tier. |
CMS recipes
WordPress
Add a Custom HTML block where you want the map and paste the install snippet. In classic-editor themes, switch the editor to Text mode first. Page builders (Elementor, Divi) all have an HTML widget that works the same way.
Squarespace
Insert a Code block (not Embed) and paste the snippet. Code blocks require a Business plan or above — on Personal plans, use the iframe recipe below.
Drupal
Paste the snippet into any field rendered with the Full HTML text format, or add it as a custom block. If your format strips <script> tags, either allow the xyzski.com source or use the iframe recipe.
Iframe fallback — for CMSes that strip scripts
The frame renders the identical viewer, hosted by us. You lose the events API but keep everything guests see. The same domain allow-list applies: the frame checks the embedding page's Referer against your license's registered domains server-side, so keep the browser's default referrer policy — an iframe with referrerpolicy="no-referrer" gets a blocked panel instead of the map.
<iframe
src="https://www.xyzski.com/embed/frame?resort=sunlight&token=demo"
title="Sunlight Mountain 3D trail map"
width="100%" height="560" style="border:0"
loading="lazy" allow="fullscreen"></iframe>How your mountain gets built
- 1
Elevation
We pull the best public DEM for your location — 1 m USGS 3DEP lidar in the US, 30 m Copernicus GLO-30 worldwide — and quantize it into a compact heightfield with 0.25 m vertical steps.
- 2
Map data
Runs, lifts, glades, boundaries, lodges, and forest cover come from OpenStreetMap and ESA WorldCover — extracted offline during the bake. The embed never calls a map API at runtime.
- 3
The bundle
Everything compiles to one static bundle (~490 KB for the demo resort) served from our CDN: two heightmap tiers, trail geometry, forest mask, and a manifest that carries the attribution strings for its own sources.
Map data © OpenStreetMap contributors (ODbL) · Terrain: USGS 3DEP (public domain) · Forest: © ESA WorldCover 2021 (CC BY 4.0) — rendered in the embed, always. Full notice at /legal/attribution.
Performance & GPU behavior
- Tiered loading. A 256-px heightfield + trails (~100 KB) paints first; the 1024-px tier streams in after the camera settles and is never downgraded or re-fetched.
- Visibility gating. The render loop runs only while the element intersects the viewport and the tab is visible. Offscreen cost: zero frames.
- Quality tiers. Software renderers (SwiftShader, llvmpipe) are detected at startup and get a lite scene with pixel ratio capped at 0.75 in the expanded view; hardware GL runs the full scene at up to 2× DPR.
- Context-loss survival. If the browser reclaims the WebGL context, the canvas stays mounted, a poster covers it, and rendering resumes on restore — no manual reload.
- Reduced motion. Under
prefers-reduced-motionthe model renders fully drawn and static: no draw-in, no camera drift.