← Gallery / Stories & animation
Build a map story
Turn a map into a guided tour. Steps fire actions on a timeline, with play, pause and scrub.
<!DOCTYPE html>
<!--
A story turns a map into a guided tour. `<om-story>` is a SIBLING of the
scene, not a wrapper: it names a timeline of steps, and each step fires the
same action a behavior or a button would fire. Delete the story and the map
is byte-for-byte the same map — which is what makes a tour something you add
to a finished map rather than something you build the map around.
Reach for it when a map has an order to it: an argument, a walkthrough, a
product tour. Anything a reader would otherwise have to be told to click.
The tour: three districts fade up together, a card over each reads
that feature's own attributes, the camera flies to each in turn while the
markup that moved it is shown beside it, then the polygons clear and a
thousand bike racks drop in one by one. Press ▶ (bottom left), or drag the
scrubber to any point — a story is seekable, not just playable.
-->
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OnlyMapJS — Build a map story</title>
<link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap/dist/onlymapjs.css" />
<script type="module" src="https://unpkg.com/@nika-js/onlymap/dist/onlymap.standalone.js"></script>
<style>
:root { color-scheme: dark; }
html, body { margin: 0; height: 100%; font-family: system-ui, sans-serif; }
/* Custom properties inherit THROUGH shadow roots where selectors cannot,
so one block here themes every built-in widget and every overlay card
below — the cards only name the tokens, never the colours. */
om-map {
display: block; height: 100vh;
--om-widget-bg: #14161c;
--om-widget-fg: #f8fafc;
--om-widget-muted: #8b93a7;
--om-widget-border: #2c3040;
--om-widget-hover-bg: #22262f;
--om-widget-accent: #22d3ee;
--card-bg: #14161c;
--card-fg: #f8fafc;
--card-muted: #8b93a7;
--card-border: #2c3040;
--card-accent: #22d3ee;
--card-shadow: 0 12px 38px rgba(0, 0, 0, .62);
--code-bg: #0b0e14;
--c-tag: #7dd3fc; --c-attr: #c4b5fd; --c-val: #86efac; --c-key: #fbbf24;
}
</style>
</head>
<body>
<!-- The opening frame holds all three polygons; the tour returns to it. -->
<om-map center="[-122.45, 37.773]" zoom="11.3" basemap="dark-matter">
<!-- Both layers start hidden, so the story opens on a bare basemap and
every reveal belongs to a step. `fade` brings a whole layer up over a
duration; `populate` drops a point layer in one marker at a time. -->
<om-layer id="regions" type="GeoJsonLayer"
data="../../data/sf-districts.geojson"
label="SF districts" color="#22d3ee"
filled stroked pickable visible="false"
get-fill-color="[34, 211, 238, 70]"
get-line-color="[103, 232, 249]"
line-width-min-pixels="2"></om-layer>
<om-layer id="bikes" type="ScatterplotLayer"
data="../../data/bikes.json"
label="Bike racks" color="#fbbf24" visible="false"
get-position="[$lon, $lat]"
get-radius="scale($size, sqrt, [3, 30], domain=[0, 100])"
radius-units="pixels" pickable></om-layer>
<!-- Anchored to a coordinate: this card belongs to the whole set, not to
one polygon, so it takes a lng/lat rather than a feature. -->
<om-overlay id="chapter" anchor="[-122.45, 37.80]" visible="false" anchor-offset="bottom-center">
<style>
.card { display:grid; grid-template-columns:minmax(155px,.8fr) minmax(255px,1.2fr); gap:14px; align-items:center;
width:min(520px,calc(100vw - 56px)); box-sizing:border-box; background:var(--card-bg); color:var(--card-fg);
border:1px solid var(--card-border); border-radius:12px; padding:14px 16px;
box-shadow:var(--card-shadow); font:13px system-ui,sans-serif; }
.card h3 { margin:0 0 4px; font-size:14px; color:var(--card-accent); }
pre { margin:0; padding:10px 12px; border-radius:8px; overflow:auto; white-space:pre-wrap;
background:var(--code-bg); border:1px solid var(--card-border);
font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace; }
.tag{color:var(--c-tag)} .attr{color:var(--c-attr)} .value{color:var(--c-val)} .key{color:var(--c-key);font-weight:700}
@media (max-width:700px){ .card{grid-template-columns:1fr;width:min(340px,calc(100vw - 32px))} }
</style>
<div class="card">
<div>
<h3>The districts</h3>
Three districts, drawn from plain GeoJSON, fading up together.
</div>
<pre><code><span class="tag"><om-step</span>
<span class="attr">duration</span>=<span class="value">"3s"</span> <span class="key">fade</span>
<span class="attr">layer</span>=<span class="value">"regions"</span><span class="tag">></om-step></span></code></pre>
</div>
</om-overlay>
<!-- Anchored to a FEATURE instead of a coordinate: name the layer and the
feature id and the card follows that polygon's own geometry, so it
stays put through every camera move. {{field}} pulls from the same
feature's properties and is HTML-escaped. -->
<om-overlay id="detail-north-beach" anchor-layer="regions" anchor-feature-id="north-beach" visible="false" anchor-offset="center">
<style>
.card { background:var(--card-bg); color:var(--card-fg); border:1px solid var(--card-border);
border-radius:12px; padding:13px 16px; min-width:190px; box-shadow:var(--card-shadow);
font:13px system-ui,sans-serif; }
.card h3 { margin:0 0 7px; font-size:14px; color:var(--card-accent); }
.row { display:flex; justify-content:space-between; gap:16px; margin-top:3px; color:var(--card-muted); }
.row span:last-child { color:var(--card-fg); font-variant-numeric:tabular-nums; font-weight:600; }
</style>
<div class="card">
<h3>{{name}}</h3>
<div class="row"><span>Population</span><span>{{population}}</span></div>
<div class="row"><span>Area</span><span>{{area_km2}} km²</span></div>
<div class="row"><span>Founded</span><span>{{founded}}</span></div>
</div>
</om-overlay>
<om-overlay id="detail-mission" anchor-layer="regions" anchor-feature-id="mission" visible="false" anchor-offset="center">
<style>
.card { background:var(--card-bg); color:var(--card-fg); border:1px solid var(--card-border);
border-radius:12px; padding:13px 16px; min-width:190px; box-shadow:var(--card-shadow);
font:13px system-ui,sans-serif; }
.card h3 { margin:0 0 7px; font-size:14px; color:var(--card-accent); }
.row { display:flex; justify-content:space-between; gap:16px; margin-top:3px; color:var(--card-muted); }
.row span:last-child { color:var(--card-fg); font-variant-numeric:tabular-nums; font-weight:600; }
</style>
<div class="card">
<h3>{{name}}</h3>
<div class="row"><span>Population</span><span>{{population}}</span></div>
<div class="row"><span>Area</span><span>{{area_km2}} km²</span></div>
<div class="row"><span>Founded</span><span>{{founded}}</span></div>
</div>
</om-overlay>
<om-overlay id="detail-sunset" anchor-layer="regions" anchor-feature-id="sunset" visible="false" anchor-offset="center">
<style>
.card { background:var(--card-bg); color:var(--card-fg); border:1px solid var(--card-border);
border-radius:12px; padding:13px 16px; min-width:190px; box-shadow:var(--card-shadow);
font:13px system-ui,sans-serif; }
.card h3 { margin:0 0 7px; font-size:14px; color:var(--card-accent); }
.row { display:flex; justify-content:space-between; gap:16px; margin-top:3px; color:var(--card-muted); }
.row span:last-child { color:var(--card-fg); font-variant-numeric:tabular-nums; font-weight:600; }
</style>
<div class="card">
<h3>{{name}}</h3>
<div class="row"><span>Population</span><span>{{population}}</span></div>
<div class="row"><span>Area</span><span>{{area_km2}} km²</span></div>
<div class="row"><span>Founded</span><span>{{founded}}</span></div>
</div>
</om-overlay>
<om-overlay id="points-card" anchor="[-122.45, 37.80]" visible="false" anchor-offset="bottom-center">
<style>
.card { display:grid; grid-template-columns:minmax(155px,.8fr) minmax(255px,1.2fr); gap:14px; align-items:center;
width:min(520px,calc(100vw - 56px)); box-sizing:border-box; background:var(--card-bg); color:var(--card-fg);
border:1px solid var(--card-border); border-radius:12px; padding:14px 16px;
box-shadow:var(--card-shadow); font:13px system-ui,sans-serif; }
.card h3 { margin:0 0 4px; font-size:14px; color:var(--card-accent); }
pre { margin:0; padding:10px 12px; border-radius:8px; overflow:auto; white-space:pre-wrap;
background:var(--code-bg); border:1px solid var(--card-border);
font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace; }
.tag{color:var(--c-tag)} .attr{color:var(--c-attr)} .value{color:var(--c-val)} .key{color:var(--c-key);font-weight:700}
@media (max-width:700px){ .card{grid-template-columns:1fr;width:min(340px,calc(100vw - 32px))} }
</style>
<div class="card">
<div>
<h3>Point layers too</h3>
A point layer can be populated in — each rack drops onto the map one by one.
</div>
<pre><code><span class="tag"><om-step</span>
<span class="attr">duration</span>=<span class="value">"3s"</span> <span class="key">populate</span>
<span class="attr">layer</span>=<span class="value">"bikes"</span> <span class="key">parallel</span><span class="tag">></om-step></span></code></pre>
</div>
</om-overlay>
<!-- These cards show the markup that is moving the camera while it moves,
which is the fastest way to read a story: the step on the left of the
screen is the step running on the right. -->
<om-overlay id="camera-control-card" anchor-layer="regions" anchor-feature-id="north-beach"
visible="false" anchor-offset="bottom-center">
<style>
.card { display:grid; grid-template-columns:minmax(165px,.8fr) minmax(270px,1.2fr); gap:14px; align-items:center;
width:min(540px,calc(100vw - 56px)); box-sizing:border-box; background:var(--card-bg); color:var(--card-fg);
border:1px solid var(--card-border); border-radius:12px; padding:14px 16px;
box-shadow:var(--card-shadow); font:13px system-ui,sans-serif; }
.card h3 { margin:0 0 4px; font-size:14px; color:var(--card-accent); }
pre { margin:0; padding:10px 12px; border-radius:8px; overflow:auto; white-space:pre-wrap;
background:var(--code-bg); border:1px solid var(--card-border);
font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace; }
.tag{color:var(--c-tag)} .attr{color:var(--c-attr)} .value{color:var(--c-val)} .key{color:var(--c-key);font-weight:700}
@media (max-width:700px){ .card{grid-template-columns:1fr;width:min(340px,calc(100vw - 32px))} }
</style>
<div class="card">
<div>
<h3>Camera control</h3>
Name a feature and the camera frames it — no coordinates, no zoom maths.
</div>
<pre><code><span class="tag"><om-step</span>
<span class="attr">action</span>=<span class="key">"zoom-to-feature"</span>
<span class="attr">layer</span>=<span class="value">"regions"</span>
<span class="attr">feature-id</span>=<span class="value">"north-beach"</span>
<span class="key">curve</span><span class="tag">></om-step></span></code></pre>
</div>
</om-overlay>
<om-overlay id="viewing-angle-card" anchor-layer="regions" anchor-feature-id="north-beach"
visible="false" anchor-offset="bottom-center">
<style>
.card { display:grid; grid-template-columns:minmax(145px,.7fr) minmax(280px,1.3fr); gap:14px; align-items:center;
width:min(540px,calc(100vw - 56px)); box-sizing:border-box; background:var(--card-bg); color:var(--card-fg);
border:1px solid var(--card-border); border-radius:12px; padding:14px 16px;
box-shadow:var(--card-shadow); font:13px system-ui,sans-serif; }
.card h3 { margin:0 0 4px; font-size:14px; color:var(--card-accent); }
pre { margin:0; padding:10px 12px; border-radius:8px; overflow:auto; white-space:pre-wrap;
background:var(--code-bg); border:1px solid var(--card-border);
font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace; }
.tag{color:var(--c-tag)} .attr{color:var(--c-attr)} .value{color:var(--c-val)} .key{color:var(--c-key);font-weight:700}
@media (max-width:700px){ .card{grid-template-columns:1fr;width:min(340px,calc(100vw - 32px))} }
</style>
<div class="card">
<div>
<h3>Camera angles</h3>
Tilt with `pitch`, swivel with `bearing` — positive turns east, negative west.
</div>
<pre><code><span class="tag"><om-step</span>
<span class="attr">action</span>=<span class="key">"fly-to"</span>
<span class="attr">pitch</span>=<span class="value">"45"</span>
<span class="attr">bearing</span>=<span class="value">"<span data-live-bearing>0</span>"</span><span class="tag">></om-step></span></code></pre>
</div>
</om-overlay>
<!-- A transform on the card shifts it clear of the polygon it is anchored
to, so the destination stays visible while the card explains it. -->
<om-overlay id="camera-smoothing-mission" anchor-layer="regions" anchor-feature-id="mission"
visible="false" anchor-offset="center">
<style>
.card { transform:translate(min(230px,18vw),90px); display:grid; grid-template-columns:minmax(140px,.8fr) minmax(220px,1.2fr);
gap:14px; align-items:center;
width:min(470px,calc(100vw - 64px)); box-sizing:border-box; background:var(--card-bg); color:var(--card-fg);
border:1px solid var(--card-border); border-radius:12px; padding:14px 16px;
box-shadow:var(--card-shadow); font:13px system-ui,sans-serif; }
pre { margin:0; padding:10px 12px; border-radius:8px; overflow:auto; white-space:pre-wrap;
background:var(--code-bg); border:1px solid var(--card-border);
font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace; }
.tag{color:var(--c-tag)} .attr{color:var(--c-attr)} .value{color:var(--c-val)} .key{color:var(--c-key);font-weight:700}
</style>
<div class="card">
<div>Easing is handled for you — `curve` arcs the move out and back in.</div>
<pre><code><span class="tag"><om-step</span>
<span class="attr">duration</span>=<span class="value">"2500ms"</span>
<span class="attr">action</span>=<span class="key">"zoom-to-feature"</span>
<span class="attr">feature-id</span>=<span class="value">"mission"</span> <span class="key">curve</span><span class="tag">></om-step></span></code></pre>
</div>
</om-overlay>
<om-overlay id="camera-smoothing-sunset" anchor-layer="regions" anchor-feature-id="sunset"
visible="false" anchor-offset="center">
<style>
.card { transform:translate(max(-230px,-18vw),90px); display:grid; grid-template-columns:minmax(140px,.8fr) minmax(220px,1.2fr);
gap:14px; align-items:center;
width:min(470px,calc(100vw - 64px)); box-sizing:border-box; background:var(--card-bg); color:var(--card-fg);
border:1px solid var(--card-border); border-radius:12px; padding:14px 16px;
box-shadow:var(--card-shadow); font:13px system-ui,sans-serif; }
pre { margin:0; padding:10px 12px; border-radius:8px; overflow:auto; white-space:pre-wrap;
background:var(--code-bg); border:1px solid var(--card-border);
font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace; }
.tag{color:var(--c-tag)} .attr{color:var(--c-attr)} .value{color:var(--c-val)} .key{color:var(--c-key);font-weight:700}
</style>
<div class="card">
<div>Easing is handled for you — `curve` arcs the move out and back in.</div>
<pre><code><span class="tag"><om-step</span>
<span class="attr">duration</span>=<span class="value">"2500ms"</span>
<span class="attr">action</span>=<span class="key">"zoom-to-feature"</span>
<span class="attr">feature-id</span>=<span class="value">"sunset"</span> <span class="key">curve</span><span class="tag">></om-step></span></code></pre>
</div>
</om-overlay>
<om-overlay id="layers-hidden-card" anchor="[-122.45, 37.80]"
visible="false" anchor-offset="bottom-center">
<style>
.card { display:grid; grid-template-columns:minmax(155px,.8fr) minmax(255px,1.2fr); gap:14px; align-items:center;
width:min(520px,calc(100vw - 56px)); box-sizing:border-box; background:var(--card-bg); color:var(--card-fg);
border:1px solid var(--card-border); border-radius:12px; padding:14px 16px;
box-shadow:var(--card-shadow); font:13px system-ui,sans-serif; }
pre { margin:0; padding:10px 12px; border-radius:8px; overflow:auto; white-space:pre-wrap;
background:var(--code-bg); border:1px solid var(--card-border);
font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace; }
.tag{color:var(--c-tag)} .attr{color:var(--c-attr)} .value{color:var(--c-val)} .key{color:var(--c-key);font-weight:700}
@media (max-width:700px){ .card{grid-template-columns:1fr;width:min(340px,calc(100vw - 32px))} }
</style>
<div class="card">
<div>A step can hide a layer as easily as reveal one, to clear the view.</div>
<pre><code><span class="tag"><om-step</span>
<span class="attr">action</span>=<span class="key">"toggle-layer"</span>
<span class="attr">layer</span>=<span class="value">"regions"</span>
<span class="attr">visible</span>=<span class="value">"false"</span><span class="tag">></om-step></span></code></pre>
</div>
</om-overlay>
<om-overlay id="end-card" anchor="[-122.45, 37.80]"
visible="false" anchor-offset="bottom-center">
<style>
.card { display:grid; grid-template-columns:minmax(175px,.85fr) minmax(235px,1.15fr); gap:14px; align-items:center;
width:min(520px,calc(100vw - 56px)); box-sizing:border-box; background:var(--card-bg); color:var(--card-fg);
border:1px solid var(--card-border); border-radius:12px; padding:14px 18px;
box-shadow:var(--card-shadow); font:14px system-ui,sans-serif; }
.package { color:var(--card-accent); font-weight:700; }
pre { margin:0; padding:10px 12px; border-radius:8px; overflow:auto; white-space:pre-wrap;
background:var(--code-bg); border:1px solid var(--card-border);
font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace; }
.command{color:var(--c-key)} .package-code{color:var(--c-val);font-weight:700}
@media (max-width:700px){ .card{grid-template-columns:1fr;width:min(340px,calc(100vw - 32px))} }
</style>
<div class="card">
<div>Try it today: check out OnlyMapJS on npm at <span class="package">@nika-js/onlymap</span></div>
<pre><code><span class="command">npm install</span> <span class="package-code">@nika-js/onlymap</span></code></pre>
</div>
</om-overlay>
<!-- The timeline. Steps run in document order; `parallel` starts a step
with the one before it instead of after, which is how a card fades in
over a camera move rather than waiting for it. `interrupt="pause"`
hands control back the moment a viewer touches the map. -->
<om-story id="tour" interrupt="pause">
<!-- Step 2: the layer turns visible and fades up in the same beat, so
nothing flashes on ahead of its own reveal. -->
<om-step action="toggle-layer" layer="regions" visible="true"></om-step>
<om-step duration="3s" fade layer="regions"></om-step>
<om-step duration="3s" action="show-overlay" target="chapter" parallel></om-step>
<!-- Step 3: dismiss the chapter card and put a detail card over each
polygon, each one reading its own feature's attributes. -->
<om-step duration="1s" action="hide-overlay" target="chapter"></om-step>
<om-step action="show-overlay" target="detail-north-beach"></om-step>
<om-step action="show-overlay" target="detail-mission" parallel></om-step>
<om-step action="show-overlay" target="detail-sunset" parallel></om-step>
<!-- Step 4: fly to each district in turn. The camera tilts, swivels
right then left, and levels off before moving on. -->
<om-step duration="2500ms" action="zoom-to-feature" layer="regions" feature-id="north-beach" curve></om-step>
<om-step duration="2500ms" action="show-overlay" target="camera-control-card" parallel></om-step>
<om-step action="hide-overlay" target="detail-north-beach" parallel></om-step>
<om-step action="hide-overlay" target="camera-control-card"></om-step>
<om-step duration="1400ms" action="fly-to" pitch="45"></om-step>
<om-step duration="1400ms" action="show-overlay" target="viewing-angle-card" parallel></om-step>
<om-step duration="1600ms" action="fly-to" bearing="28"></om-step>
<om-step duration="2400ms" action="fly-to" bearing="-28"></om-step>
<om-step duration="1600ms" action="fly-to" bearing="0"></om-step>
<om-step duration="1200ms" action="fly-to" pitch="0" bearing="0"></om-step>
<om-step action="hide-overlay" target="viewing-angle-card" parallel></om-step>
<om-step duration="2500ms" action="zoom-to-feature" layer="regions" feature-id="mission" curve></om-step>
<om-step duration="4500ms" action="show-overlay" target="camera-smoothing-mission" parallel></om-step>
<om-step action="hide-overlay" target="camera-smoothing-mission"></om-step>
<om-step duration="2500ms" action="zoom-to-feature" layer="regions" feature-id="sunset" curve></om-step>
<om-step duration="4000ms" action="show-overlay" target="camera-smoothing-sunset" parallel></om-step>
<om-step action="hide-overlay" target="camera-smoothing-sunset"></om-step>
<!-- Step 5: clear the cards and the polygons, return to the opening
frame, and drop the point layer in one marker at a time. -->
<om-step action="hide-overlay" target="detail-north-beach"></om-step>
<om-step action="hide-overlay" target="detail-mission" parallel></om-step>
<om-step action="hide-overlay" target="detail-sunset" parallel></om-step>
<om-step duration="3s" action="fly-to" center="[-122.45, 37.773]" zoom="11.3" curve parallel></om-step>
<om-step action="toggle-layer" layer="regions" visible="false"></om-step>
<om-step duration="2200ms" action="show-overlay" target="layers-hidden-card" parallel></om-step>
<om-step action="hide-overlay" target="layers-hidden-card"></om-step>
<om-step action="toggle-layer" layer="bikes" visible="true"></om-step>
<om-step duration="3s" populate layer="bikes" parallel></om-step>
<om-step duration="3s" action="show-overlay" target="points-card" parallel></om-step>
<om-step action="hide-overlay" target="points-card"></om-step>
<om-step action="show-overlay" target="end-card"></om-step>
</om-story>
<!-- A widget sits in a managed slot, so this title holds its corner while
the geo-anchored cards move with the map underneath it. -->
<om-widget position="top-left" order="-10">
<style>
.story-title { padding:11px 15px; border:1px solid var(--card-border); border-radius:12px;
background:rgba(20,22,28,.86); color:var(--card-fg); box-shadow:var(--card-shadow);
font:650 15px/1.25 system-ui,sans-serif; letter-spacing:.01em; backdrop-filter:blur(10px); }
.story-title span { display:block; margin-top:3px; font:11px/1.3 system-ui,sans-serif;
letter-spacing:.08em; text-transform:uppercase; color:var(--card-accent); }
</style>
<div class="story-title">
San Francisco, three districts
<span>A map story</span>
</div>
</om-widget>
<!-- The player is ordinary chrome — it emits story-play, story-pause and
story-seek, so your own transport controls can replace it. -->
<om-widget type="player" story="tour" position="bottom-left"></om-widget>
<om-widget type="legend" title="Layers" position="top-right"></om-widget>
<om-fallback>
<p style="font: 15px system-ui, sans-serif; padding: 24px; max-width: 42ch">
<strong>This map needs JavaScript.</strong><br />
Open this file in a web browser such as Chrome, Safari, or Firefox.
</p>
</om-fallback>
</om-map>
<script type="module">
// The card above quotes the bearing the story is flying to. This keeps the
// quoted number in step with the camera as it interpolates, so the sample
// never disagrees with the map behind it: positive swivels east, negative
// west. Reading the camera is all it does — nothing here drives the map.
await customElements.whenDefined("om-overlay");
const map = document.querySelector("om-map");
const angleCard = document.querySelector("#viewing-angle-card");
let bearingFrame = 0;
const updateBearing = () => {
if (angleCard.getAttribute("visible") !== "true") {
bearingFrame = 0;
return;
}
const output = angleCard.shadowRoot?.querySelector("[data-live-bearing]");
const bearing = map.getViewStateInternal()?.bearing;
if (output && bearing != null) output.textContent = String(Math.round(bearing));
bearingFrame = requestAnimationFrame(updateBearing);
};
new MutationObserver(() => {
if (angleCard.getAttribute("visible") === "true" && !bearingFrame) updateBearing();
}).observe(angleCard, { attributes: true, attributeFilter: ["visible"] });
</script>
</body>
</html>
The snippet above loads the latest release. The demo above it is pinned to v0.5.2, so it keeps working when a new version ships.