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.
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.
The viewer emits a typed event stream; the web component re-dispatches it on the element as xyzski:event (plus a one-time xyzski:loaded), and the JS API hands you the raw callback:
// Web component: the element re-dispatches the stream as DOM events.
const el = document.querySelector('xyzski-mountain')
el.addEventListener('xyzski:loaded', () => console.log('terrain ready'))
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)
})
// JS API: the same stream as a raw callback.
const viewer = createMountainViewer(container, {
manifestUrl: 'https://www.xyzski.com/resorts/sunlight/manifest.json',
onEvent: (e) => { /* same events, no DOM hop */ },
})| type | payload | fires when |
|---|---|---|
| loaded | { manifest, quality } | Bundle parsed and first frame rendered. quality is 'full' or 'lite' (software-GPU fork). |
| 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. |
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.
<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.