Skip to content
xyzski

Docs

How it works.

Six plates, in install order. Written for the person who will actually paste the code.

01

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.

02

Attributes reference

Web-component configuration.

AttributeTypeDefaultDoes
resortslugrequiredWhich mountain to render — assigned when we bake your bundle (e.g. sunlight).
tokenstringrequiredYour license token. Validated against your registered domains; the special token demo works on xyzski.com and localhost only.
themepreset | JSON"mono"Visual theme: the monochrome ink default, classic trail-map colors, or a JSON theme object (Ridgeline+).
interactivebooleantrueEnables orbit and hover picking in the expanded view. Set false for a purely ambient hero.
expandedbooleanfalseStart 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).

03

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 */ },
})
typepayloadfires 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.
04

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>
05

How your mountain gets built

  1. 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. 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. 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.

06

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-motion the model renders fully drawn and static: no draw-in, no camera drift.

Questions your team will ask? Bring them.