Site embed
/siteembed/{mapId} is a destination map that your website drives as visitors move between pages. An article can show only the places it mentions. A home page can keep the whole destination in view and lift its featured places out. A place page can draw that place’s parking, trailheads and other related places. Changing what the map shows never reloads the iframe.
This page describes Site Embed 2.1.0: the embed reports embedVersion: '2.1.0' in its ready event. 2.1.0 adds the story-map features (lines, runtime terrain, a dimmed base map, pin accents and a staggered reveal) and lists them in ready.capabilities. Everything else behaves as in 2.0.0, and a host that sends none of the new commands sees no difference. /siteembed-next/{mapId} is a temporary alias of the same embed and will be removed, so always use /siteembed/.
Most hosts drive the embed with the Embed SDK (Wander.siteMap). A plain iframe works too: it shows the whole map, and visitors can search, pan and open places, but your page can’t steer it.
Quick start
Section titled “Quick start”With the SDK, pinned to a version (see Pinning a version):
<div id="map" style="position: relative; height: 600px"></div><script src="https://developer.wandermaps.com/sdk/wander-embed-0.10.0.js" integrity="sha384-60kCl7EeS2FmqBQdS/KZGdq8gUObbt/1uZEhGOLto0A5e+7/o5b4XoQLZA7emRXw" crossorigin="anonymous"></script><script> const site = Wander.siteMap('#map', { mapId: 'YOUR-MAP-ID', namespace: 'my-site', fill: true, background: '#ECE5D6' });
site.on('ready', ({ embedVersion }) => console.log('Map ready, embed', embedVersion)); site.on('poiSelected', ({ poiId, source }) => { if (source === 'user') openPlacePage(poiId); });
// Each page tells the map what it is about. site.setContext([ { poiId: 'PLACE-ID-1', role: 'featured', badge: '1' }, { poiId: 'PLACE-ID-2', role: 'mentioned', badge: '2' } ], { mode: 'only', fit: true });</script>As a plain iframe:
<iframe src="https://web.wander-app.com/siteembed/YOUR-MAP-ID?bg=%23ECE5D6" title="Destination map" allow="geolocation" style="display: block; width: 100%; height: 600px; border: 0"></iframe>URL parameters
Section titled “URL parameters”The SDK builds this URL for you. A plain iframe sets the parameters itself.
| Parameter | Values | Default | Meaning |
|---|---|---|---|
ns | String | default | Namespace. Every command must carry it, and every event carries it. |
pois | Comma-separated place IDs | Empty | Initial page context. Each ID starts as a mentioned place with no badge, and the map doesn’t fit to them. Use setContext for roles, badges and fitting. |
mode | only, emphasize, all | all | Initial display mode. Any other value means all. The SDK always sends a mode, emphasize unless you choose another, so an SDK map and a plain iframe start differently. |
showControls | false | Shown | Only false hides the zoom, compass, geolocate and recenter controls. |
showSearch | false | Shown | Only false hides the search bar. The embed’s analytics come only from search, so this turns them off too. |
associationsInCard | false | Shown | Only false hides the related-place pills in the place card, for hosts that list related places themselves. The map still draws associations. |
controlsPosition | top-left, top-right, bottom-left, bottom-right | bottom-right | Corner for the map controls. With top-right, the search bar moves to the top left. |
bg | Hex colour, #RGB or #RRGGBB; # optional, URL-encoded as %23 | Unset | Paints the map container and the map style’s background, so your page colour shows instead of black until the tiles paint. Other values are ignored. |
fit | boundary, point | Venue maps fit their boundary; other maps open at the map’s own point | How the map opens. boundary fits the map’s boundary (40 px padding, maximum zoom 18) when the map has one. Any usable lat, lng or z (see below) overrides it. |
lat, lng, z, b, p | Numbers | The map’s own camera | Initial latitude, longitude, zoom, bearing and pitch. For lat, lng and z, 0, an empty value or a value that isn’t a number counts as unset, so a missing value in your code can’t open the map at 0°, 0° or zoomed out to the whole world. b and p use any finite number, 0 included: b=0&p=0 gives a north-up, flat camera even when the map has its own bearing or pitch. |
areaLabels | outline, center | outline | How areas are labelled. outline labels each area along its outline. center puts the area’s name in its middle and hides the pins of areas and trails, for maps that are mostly areas, such as fields or zones. |
sdk | Version, such as 0.10.0 | Unset | Set by the SDK (0.8.0 and later) to its own version. The embed reports it back as ready.sdkVersion. Plain iframes leave it out. |
debug | Present or absent | Absent | Exposes window.__seDebug inside the iframe (padding, zoom, container size and project) and logs camera fits to the console. For testing only. |
hideSignIn, theme | Any | Unset | Accepted for compatibility and ignored. The site embed has no sign-in UI and one theme. |
Bridge protocol
Section titled “Bridge protocol”The host posts commands to the iframe’s window in this envelope:
{ source: 'wander-embed', v: 1, namespace, type, payload }The embed posts events to window.parent with target origin '*', in the same envelope with source: 'wander-embed-child'. The protocol version is 1 (Wander.PROTOCOL). New commands and events are additive, so an embed ignores verbs it doesn’t know.
The embed acts on a command only when:
- the envelope is valid:
sourceis exactly'wander-embed',vis1,namespacematchesns, andpayloadis an object; - the payload is valid for that command. An invalid payload is ignored without an
ackor an error, except for the story-map commands, which answer it witherror { reason: 'invalid_payload' }and anack; - the map has finished loading. Commands that arrive earlier are dropped. The SDK holds commands until
ready, so SDK hosts never lose one.
There is no origin check. A handled command is answered with ack { type }, posted to the window that sent it at that window’s origin, or to '*' when the sender’s origin is the opaque "null" of a file:// page, so local demo pages work. flyTo is acked only when it has a place to move to, and fitBounds only when there was something to fit. storyRoute, storyRouteProgress, setSections and highlightSection are never acked. The story-map commands (setLines, animateLine, setLineProgress, setTerrain, resetTerrain and setAppearance) are always acked, whether they were applied or rejected.
A disabled map shows a “map unavailable” screen and sends nothing, not even loading.
Commands
Section titled “Commands”| Command | Payload | What it does |
|---|---|---|
setContext | { items, mode?, fit?, padding?, reveal? } | Replaces the page context with items (see Context items). It also clears any external place, highlight, focus stack (focusChanged { reason: 'clear' } when the stack wasn’t empty) and selection (poiDeselected { source: 'host' }). It fetches only the IDs it hasn’t loaded yet, in batches of up to 200, one batch at a time. fit: true fits the context places inside the current padding. padding may give any of the four sides; they are merged into the stored padding and kept for later calls. It always answers with contextApplied, then error { scope: 'context' } if a batch failed, then ack. A story route is not affected. reveal: { stagger } (2.1.0) pops the entering pins in one after another; see Pin accent and reveal. |
setMode | { mode } | Changes the display mode and resets the page as setContext does, without fetching. Answers with contextApplied. |
setPadding | { top, right, bottom, left }, or { padding: { top, right, bottom, left } } | Sets the camera padding and keeps it. All four sides must be finite numbers; anything else is ignored. The same value again does nothing (it is still acked). A change while the camera is moving waits for the move to end, and the latest value wins. See Camera padding. |
hover | { poiId: string | null } | Shows the hover state for a place in the page context or among the map’s loaded places, and echoes poiHovered { poiId }. null, or an ID the embed hasn’t loaded, clears hover and echoes poiUnhovered {}. |
highlight | { poiId: string | null } | Highlights an association target of the current anchors: it makes the target’s type active, sends associationHighlighted { source: 'host' }, pulses the target and fits the anchor and target together (maximum zoom 15, or 17 when the target is a trail). Any other ID is highlighted without an event. null clears the highlight and the active type. Repeating the current highlight does nothing. The highlight only stays while a place is selected (see Behaviours to rely on). |
fitBounds | { poiIds?, padding? } | Fits the listed places, looked up in the page context and then among the loaded places. Without poiIds it fits the current context (with an empty context, the places loaded for earlier contexts). padding sides are merged into the stored padding and kept. Not acked when nothing can be fitted. |
flyTo | { poiId?, center?: { lng, lat }, zoom?, bearing?, pitch?, duration?, select? } | Moves the camera to center, or else to poiId (page context, then loaded places). Only finite zoom, bearing and pitch values are used. duration is in milliseconds, clamped to 0–5000, default 900. duration: 0 jumps instantly and keeps the point centred over terrain; use it for camera streams sent every frame. select: true also selects poiId (poiSelected { source: 'host' }). Without a usable centre nothing moves and there is no ack. |
select | { poiId, fitAssociations? } | Opens a place. With an empty focus stack it becomes the anchor, exactly as with focus; fitAssociations: true then fits it with its associations once they load. Otherwise it is a peek: its card opens and the anchor’s associations stay. A selection you send never pans the map. |
focus | { poiId } or { poiIds }, plus fit? | Send exactly one of poiId or a non-empty poiIds. Makes those places the anchors and pushes them onto the focus stack, unless they are the same set as the current anchors in any order. A single anchor is selected; several anchors clear the selection. It always sends focusChanged { reason: 'focus' }, even when nothing was pushed. fit: true fits the anchors and their association targets once they load (maximum zoom 15). |
back | {} | Pops the focus stack. The previous anchors are redrawn from cache, and a single previous anchor is selected again. Sends focusChanged { reason: 'back' }. On an empty stack it is acked and nothing else happens. |
clearFocus | {} | Empties the focus stack (focusChanged { poiIds: [], depth: 0, reason: 'clear' }, only when it wasn’t empty) and always clears the selection. |
clearSelection | {} | Deselects twice on purpose: a peek falls back to its anchor, and then that closes too, so nothing stays selected. The focus stack is kept. |
getAssociations | { poiId } | Replies with associations { poiId, items } from the embed’s cache, fetching if needed. It never selects or draws anything. An ID with no associations, or one that isn’t a place at all, gets items: []. A failed fetch sends error { scope: 'associations', poiId } and no reply. The ack comes after the reply. |
storyRoute | { stops: [[lng, lat], …], profile?, color? } | Loads an itinerary route; see Itinerary story routes. Fewer than two valid stops clears the route. |
storyRouteProgress | { at } | Draws the route up to at: the number of legs completed plus the fraction of the current leg. |
The path-section commands setSections and highlightSection need SDK 0.9.0 and are described in Path sections. The story-map commands setLines, animateLine, setLineProgress, setTerrain, resetTerrain and setAppearance need embed 2.1.0 and are described in Story maps.
Context items
Section titled “Context items”{ poiId: 'f56b236a-1bb8-4afe-a302-d84b564a4cf6', role: 'featured', badge: '1' }| Field | Required | Meaning |
|---|---|---|
poiId | Yes | A place on this map. IDs that aren’t on this map come back in contextApplied.missing. |
role | Yes | featured or mentioned. A featured place always stays a named photo marker and never joins a cluster, and its name gets label space first. A mentioned place can join a cluster. |
badge | No | One to three characters drawn in a circle on the place’s photo marker, such as a step number. Any other value is removed from the item, and the item still applies. A place without a photo shows no badge. |
geometry | No | marker, path or polygon. Rarely needed: the embed works it out. |
accent | No | 'glow': the place’s pin gets a soft, slowly breathing halo. 2.1.0 only; see Pin accent and reveal. Any other value is removed from the item, and the item still applies. |
accentColor | No | #RRGGBB colour for the accent. Defaults to the map’s primary colour. Any other value is removed. |
Every item must be an object with a string poiId and a valid role. If any item isn’t (a plain ID string, or a missing role), the embed ignores the whole command: no contextApplied and no ack. When an ID repeats, the place keeps its first position and the last item’s role and badge win.
Events
Section titled “Events”| Event | Payload | When it fires |
|---|---|---|
loading | { namespace, mapId } | Once, as soon as the embed page starts, before the map style loads. |
ready | { namespace, mapId, surface: 'siteembed', poiCount, mode, embedVersion, sdkVersion, capabilities } | Once, on Mapbox’s load event: the style has loaded and the first tiles have painted. embedVersion is '2.1.0'. capabilities lists the optional features this embed supports: ['lines', 'terrain', 'appearance', 'accent', 'reveal'] (2.1.0; absent from 2.0.0). sdkVersion is the SDK version from the sdk= parameter, or null (for a plain iframe, or a value that isn’t a version). mapId is the ID or slug exactly as it appears in the URL. poiCount is the number of context places loaded at that moment. mode is the starting mode. |
ack | { type } | A command was handled. |
contextApplied | { count, mode, missing } | After every setContext and setMode, including empty contexts. missing lists the IDs that didn’t load (not on this map, or their batch failed). count is the number of items minus missing. |
poiSelected | { poiId, source } | The selection changed to a place. source is 'host' when one of your commands caused it, and 'user' for a tap, a card button, a search pick or a pick from a cluster preview. |
poiDeselected | { source } | The selection was cleared, with the same source rule. |
poiHovered | { poiId } | The visitor’s pointer moved onto a pin, trail, area, landmark or photo marker; also the echo of hover. |
poiUnhovered | {} | Hover ended; also the echo of hover(null). |
clusterClicked | { count, bounds: { n, s, e, w } } | A cluster was clicked. The embed zooms in by itself, or opens its list when zooming can’t separate the places, so don’t zoom in response. For a stack of places at one spot, bounds is that single point. |
focusChanged | { poiIds, depth, reason } | Every push, pop or clear of the focus stack, from your commands or the visitor. reason is 'focus', 'back' or 'clear'. A peek never sends it. |
associationsLoaded | { poiId, poiIds, count, byCategory } | The anchors’ associations have loaded. poiId is the first anchor and poiIds all of them. byCategory counts all eight categories, zeros included: access, serves, practical, safety, enhancement, sequence, alternative, context. |
associations | { poiId, items } | The reply to getAssociations. See Association rows. |
associationSelected | { fromPoiId, toPoiId, type } | The newly selected place is an association target of the current anchors, however it was selected: a tap on its pin, a row in the card, select or flyTo { select: true }. It fires just before that place’s poiSelected. |
associationHighlighted | { fromPoiId, toPoiId, type, source } | A target was highlighted, by a first tap ('user') or by highlight ('host'). |
associationTypeSelected | { poiId, type } | The active association type changed while a place is selected, from a type pill in the card or a highlight. poiId is the selected place. |
associationTypeCleared | {} | The active association type was cleared. |
storyRouteLoaded | { fallback, legs } | The reply to storyRoute. legs is [{ meters, seconds, coordinates }], one leg per pair of stops. |
searchPerformed | { query, mapResultCount, externalResultCount, partial } | Each search response. partial: true means places elsewhere were unavailable, so only results on this map were shown. |
searchResultSelected | { query, poiId } | The visitor picked a result on this map. The usual poiSelected { source: 'user' } follows. |
externalPlaceSelected | { query, placeId, name, address?, lng, lat, photo?, rating?, source: 'google' } | The visitor picked a place that isn’t on this map. See Search. |
externalPlaceCleared | {} | The pin for that place was removed. |
boundsChanged | { center: { lng, lat }, zoom, bounds: { n, s, e, w }, pitch, bearing } | 250 ms after every camera move ends, including moves you commanded. pitch and bearing are in degrees (2.1.0). |
dimensionChanged | { width, height } | On load and on resize. height is a suggestion, 0.72 × width clamped to 260–560 px, not a measured height. The SDK doesn’t apply it to site maps. |
error | See Errors |
The path-section events sectionsApplied, sectionHighlighted and sectionSelected need SDK 0.9.0 and are described in Path sections. The story-map events linesApplied, lineAnimationEnded, terrainChanged and appearanceChanged need embed 2.1.0 (and SDK 0.10.0 to subscribe with on) and are described in Story maps.
Errors
Section titled “Errors”| Payload | When |
|---|---|
{ scope: 'context', poiIds } | A batch failed while setContext was loading places. It comes right after that command’s contextApplied. poiIds lists the failed batch and every batch after it. Those IDs are also in missing, and the next setContext tries them again. |
{ scope: 'associations', poiId, poiIds } | The anchors’ associations failed to load. poiId is the first anchor and poiIds all of them. The card still opens. |
{ scope: 'associations', poiId } | A getAssociations fetch failed. No associations reply follows, so the SDK’s promise rejects when it times out. |
{ scope: 'section', … } | A path-section command failed. See Path sections. |
{ scope: 'line' | 'terrain' | 'appearance', reason, command } | A story-map command was rejected. command is the message type and reason a stable string. See Story-map errors. |
Aborted fetches are silent. There is no event for a map that fails to load: if ready never arrives, your page has to time out on its own.
Association rows
Section titled “Association rows”Each item in associations.items is a raw association row, reverse edges included:
{ id, association_type, is_navigational_target, display_label, notes, sort_order, list_id, direction, // 'forward' or 'reverse' relation_label, // optional display_group, // optional associated_poi: { id, type, name, coordinates } // type: 'marker', 'path' or 'polygon'; coordinates: [lng, lat]}Behaviour guide
Section titled “Behaviour guide”Display modes
Section titled “Display modes”| Mode | Page places | Other places |
|---|---|---|
only | Photo markers. mentioned places join clusters only when the context has more than 80 places | Hidden, with their trails and areas. Landmarks and the boundary mask still show |
emphasize | Photo markers; mentioned places can join clusters | Small pins without labels. Trails and areas stay drawn |
all | Photo markers; mentioned places can join clusters | Normal pins with labels |
A photo marker uses the place’s first photo. An anchor without a photo uses the map’s logo. Any other page place without a photo shows a category pin with the category’s glyph or the place’s first letter. A page place that is a trail or an area shows its pin and also draws its line or shape, in every mode, only included.
Clusters
Section titled “Clusters”Places that crowd together merge into a donut: a white disc showing the count, with a coloured ring.
- In
all, the ring is split by category colour. - In
emphasizetoo, the ring is split by category colour. A donut that contains at least one page place also gets a thin outer rim in the map’s primary colour, with a white gap, and its accessible name adds ”, includes N places on this page”. Inonlyevery clustered place is a page place, so there is no rim.
Every place that isn’t in the page context can join a donut, trails and areas included: their pins join, while their lines and shapes stay drawn at every zoom. mentioned page places join too (in only, only past 80 page places). featured places never do. A selected, highlighted or hovered place is drawn as its own marker above its donut, and the count doesn’t change. Each map sets its cluster radius and maximum cluster zoom (by default 44 px and zoom 15). Beyond that zoom, places at exactly the same spot become one stacked pin with a count.
- Hover (mouse or trackpad) opens a preview. Up to four places show as a list with their full names; more show as a grid of photo tiles (up to eight, or seven and a “+N” tile). Page places are listed first. Picking a place flies in to a zoom where it stands alone (700 ms) and then selects it, sending
poiSelected { source: 'user' }. Hovering a donut sends no event. - Click or tap sends
clusterClicked, then zooms in to where the cluster breaks apart (500 ms). When zooming can’t separate the places, the preview opens and stays open instead. A click on a stacked pin sendsclusterClickedwith a single-pointboundsand opens its list. On touch screens there is no hover preview: a tap zooms. hover(poiId)for a place inside a donut marks that donut.
Any place the embed has loaded can be hovered: the page context, and the map’s places loaded for the area the map has shown. Where trails, areas and landmarks overlap, the one drawn on top is hovered; moving onto a different one switches after 80 ms, so a pointer resting on an edge doesn’t flicker. Nothing new is hovered while the map is moving. The visitor’s pointer and hover(poiId) behave the same way: the place lifts out as a marker showing its name, and poiHovered is sent. A place the embed hasn’t loaded counts as an unhover.
Selection and the place card
Section titled “Selection and the place card”A place is selected by a tap, select, focus with one ID, flyTo { select: true }, a search pick or a cluster-preview pick. The selected pin is drawn full size, with a ring and a short pulse, and pins piled on top of it fade.
The place card appears only when the visible map is at least 480 px wide (the iframe’s width minus the left and right padding). Below that there is no card: selection events still fire, and your page shows its own UI. The card is 260 px wide. It sits centred above or below the pin when there is room, then beside it, and diagonally only as a last resort, with a tail pointing at the pin. It shows the place’s photo, name and category; Directions (to the parking lot or launch when the place has one); audio and video when the place has them; and related-place pills grouped by type, unless associationsInCard=false. A peek card adds Focus here and Back to ‹anchor›. An anchor with an earlier anchor beneath it adds Back.
The card’s ×, the Escape key and clearSelection close a peek and its anchor together. A click on an empty part of the map clears the selection, the hover and any external place. A click on a trail or an area selects it (poiSelected { source: 'user' }; a peek when a place is anchored). A tap on a path section clears an open selection in every mode. A selected area is filled gold with a heavier outline; a hovered area gets a white outline. When the visitor selects a place, the map pans just enough to keep the pin 48 px inside the padded view. Selections you send never move the camera.
Other width rules: under 640 px, labels are compact, the search bar becomes a full-width strip and the “Powered by Wander” badge is hidden. Association label chips are hidden when the visible map is under 320 px wide.
Associations: anchor, highlight and peek
Section titled “Associations: anchor, highlight and peek”Associations are typed links between places: parking, trailhead, insider tip, food nearby, historically connected and more. The anchor set, usually the one place a page is about, sits on a focus stack.
While a place is selected, the map draws the anchors’ associations by decorating each target’s own pin: a ring in the category colour, a type glyph and a label chip. Access links also get a dotted connector from the anchor. A target the map hasn’t loaded gets a standalone glyph. On a wide map, the card’s type pills choose which type the map shows.
- A first tap on a target, on its pin or its row in the card, highlights it: the pin pulses, its label shows, the map fits the anchor and target together, and
associationHighlighted { source: 'user' }is sent. - A second tap, or the row’s Details ›, opens a peek: the target’s card opens, the anchor’s associations stay where they were, and
associationSelectedis followed bypoiSelected. - Focus here makes the peeked place the new anchor and pushes the old one onto the stack. Back returns to the previous anchor from cache.
Opening any card while nothing is anchored makes that place the anchor, so a visitor’s first click always shows associations. Use emphasize rather than only on pages that show associations, so the rest of the destination stays visible.
| Category | Types |
|---|---|
access | trailhead, parking, entrance, accessible_entrance, visitor_center, transit_stop, rideshare_dropoff, boat_launch |
serves | Set by a row’s display_group |
practical | food_nearby, water_refill, restroom, ticket_purchase, gear_rental |
safety | emergency_exit, cell_signal, meeting_point, ranger_station |
enhancement | best_viewpoint, photo_spot, hidden_gem, insider_tip, sunrise_spot |
sequence | do_before, do_after, pairs_with |
alternative | when_closed, rainy_day, less_crowded, similar_vibe |
context | historically_connected, part_of_complex, featured_in, personal_note, and any type not listed here |
A row’s display_group, when it names one of these categories, overrides the category of its type.
Itinerary story routes
Section titled “Itinerary story routes”A story page, such as a scroll-driven itinerary, sends its stops once with storyRoute (SDK: storyRoute(stops, { profile, color })). The embed asks Mapbox Directions for a route (driving by default, or walking or cycling) and replies with storyRouteLoaded:
legshas one entry per pair of stops:{ meters, seconds, coordinates }, with the ends snapped to the stops;- if Directions fails, the legs are straight lines, with
fallback: trueandseconds: null.
Nothing is drawn until you send progress. storyRouteProgress(at) draws the route up to at, the number of legs completed plus the fraction of the current leg: with three stops, 1.5 is halfway along the second leg. The embed draws a faint line for the whole route, the played part as a solid line, and, mid-leg, a head dot and a leg label such as “2.3 mi · 6 min”. Only your progress moves the route; time never does.
- Progress that changes by less than 0.00001 is ignored, and progress sent before
storyRouteLoadedisn’t drawn. - A new
storyRoutereplaces one that is still loading.clearStoryRoute()(raw:storyRoute { stops: [] }) removes the route. coloris any CSS colour (default#1105cc) and stays until you send another.setContextandsetModedon’t remove the route.- For the camera, send
flyTo({ center, zoom, bearing, pitch, duration: 0 })each frame. A zero duration jumps without animating and stays centred over terrain.
Hosts that post raw messages instead of using SDK 0.8.0 or later can keep doing so: the commands and payloads are the same.
Camera padding
Section titled “Camera padding”Padding describes the parts of the iframe that your page covers: a side panel, a bottom sheet, a header. The embed treats the rest as the visible map. Fits, centring, card placement, the search bar, the map controls, the “Powered by Wander” badge and cluster previews all respect it.
setPaddingneeds all four sides. ThepaddinginsetContextandfitBoundsmay give only some sides, and those are kept for later calls too.- The map starts with 60 px on every side until you set padding.
- Sending the same padding again does nothing. A change while the camera is moving is applied when the move ends, and the latest value wins, so you can send padding every animation frame while a panel is dragged.
- A fit is computed from the padding when it starts, plus a margin that shrinks on small maps. When it ends, your padding is restored without moving the map.
- If the padding changes while a fit is animating, the embed applies it when the fit ends and, if the fit ran to completion, fits again for the new padding (400 ms, at most twice in a row). Any other camera move in between cancels that re-fit. A padding change after a fit has settled re-centres the map without fitting again.
Landmarks
Section titled “Landmarks”Landmark artwork shows in every mode, only included. Until its artwork has loaded, a landmark is drawn as a normal pin. Hovering lifts the artwork slightly, without a photo bubble; a selected landmark lifts a little more. Clicking a landmark selects it like any other place.
Search and places elsewhere
Section titled “Search and places elsewhere”At 640 px and wider, the search bar is 280 px wide at the top right, or at the top left when controlsPosition=top-right. Below 640 px it is a full-width strip. It sits inside your padding. Searching starts at two characters.
Results come in two groups. On this map lists this map’s places. Elsewhere lists Google places that aren’t on this map, even when they lie inside the map’s boundary.
- Picking a place on this map flies to it and selects it:
searchResultSelected, thenpoiSelected { source: 'user' }. - Picking a place elsewhere shows one temporary pin with a small card (name, address and Directions) and sends
externalPlaceSelected. The card’s ×, a click on the map, a new pick,setContextandsetModeremove it, sendingexternalPlaceCleared. The embed never adds the place to a trip; your page can:
site.on('externalPlaceSelected', (e) => addToTrip({ id: 'google:' + e.placeId, name: e.name, photo: e.photo ?? null, category: e.address ?? 'Outside the map'}));Loading
Section titled “Loading”loading fires as soon as the embed page starts. ready fires on Mapbox’s load event, once the style and the first tiles have painted. On a satellite style over a slow connection, that can take 30–60 seconds. Until then:
- set
background(SDK) orbg(URL) to your page colour, so the frame never shows black; - for a full-bleed map, use
fill: true; - show your own skeleton on
loadingand hide it onready.dimensionChangedis not a readiness signal.
A disabled map sends no events at all, and there’s no failure event, so give ready a timeout of your own.
Path sections
Section titled “Path sections”A path section is a named stretch of a trail or creek that is set up in Wander, such as a regulation zone, a closure or a difficulty grade. Your page chooses which sections the map shows and which one is highlighted, and the visitor can tap one.
Where sections come from. Sections arrive with their trail in the map’s place feed: the places loaded for the area the map has shown. The embed remembers every section it has seen for the rest of the page’s life. If a command names a section whose trail hasn’t loaded, the embed fetches that section’s trail (once per section), then runs the command, so you don’t need to move the map first. Only an ID that isn’t on this map comes back in missing. Section commands sent before the first places load, or while such a fetch runs, are held, and the latest setSections and the latest highlightSection run when it finishes.
| SDK method | Raw command | What it does |
|---|---|---|
setSections(items, { fit }) | setSections { sections, fit? } | Replaces the sections on the map. Each item is a section ID string, or { sectionId, label?, badge?, color? }: label (up to 60 characters) replaces the section’s name, badge (one to three characters) replaces its short label, and color (#RRGGBB) replaces its colour. The SDK throws on an invalid item. Answers with sectionsApplied. fit: true fits the sections shown (maximum zoom 15). |
highlightSection(sectionId, { fit }) | highlightSection { sectionId, fit? } | Highlights one section, adding it to the map first if it isn’t shown (without overrides). Pass null to clear the highlight. fit: true fits that section (maximum zoom 16). The SDK throws on an ID that is empty or not a string. |
clearSections() | setSections { sections: [] } | Removes every section, and with them the highlight. |
| Event | Payload | When it fires |
|---|---|---|
sectionsApplied | { sectionIds, missing } | After every setSections. sectionIds lists the sections shown, in the order sent. missing lists the IDs the embed doesn’t know. |
sectionHighlighted | { sectionId, source } | The highlighted section changed. source is 'host' for highlightSection, and also when setSections leaves out the highlighted section (then sectionId is null, sent after sectionsApplied). source is 'user' when the visitor taps a section. |
sectionSelected | { sectionId, pathId } | The visitor tapped a section on the map. It follows that tap’s sectionHighlighted { source: 'user' }. pathId is the section’s trail. |
error | { scope: 'section', sectionId, reason: 'unknown_section' } | highlightSection named a section that isn’t on this map. |
error | { scope: 'section', reason: 'invalid_payload', command } | A raw setSections or highlightSection message had an invalid payload. command is the message type. The SDK validates first, so SDK hosts don’t see this. |
- Neither command is acked: rely on
sectionsAppliedandsectionHighlighted. highlightSectionfor the section that is already highlighted sends nothing, and so doeshighlightSection(null)when nothing is highlighted. A visitor’s tap always sends both events, even on the highlighted section.- Each section is drawn over a faint copy of its whole trail, in the item’s
color, else the section’s own colour, else a colour for its kind. It has a dot at each end and its badge at its midpoint. The highlighted section is drawn wider, with a white edge and the label “badge · label”, and the other sections fade. - Because a section draws its own copy of its trail, it shows in every mode, including
onlywhen its trail isn’t in the page context. - Sections don’t depend on the page context:
setContext,setModeandclearFocusleave them as they are. CallclearSections()when a page no longer needs them. - Tapping a section doesn’t select its trail (no
poiSelected), and it closes an open place card (poiDeselected) in every mode. Hovering a section may also reportpoiHoveredfor its trail. Pins, landmarks, the story route and context labels drawn above a section take the click instead.
site.on('sectionsApplied', ({ missing }) => { if (missing.length) console.warn('Sections not on this map', missing);});site.on('sectionSelected', ({ sectionId }) => showSectionDetails(sectionId));
site.setSections([ { sectionId: 'SECTION-ID-1', badge: 'A', label: 'Catch and release only' }, 'SECTION-ID-2'], { fit: true });site.highlightSection('SECTION-ID-1');Story maps
Section titled “Story maps”A scroll-driven story page can make the map follow the text: draw a creek as the visitor reads about it, stand the mountains up for one chapter, let night fall, and make the places of a chapter glow and pop in one after another.
| Capability | Commands | Use it for |
|---|---|---|
lines | setLines, animateLine, setLineProgress | Drawing any line, from a trail’s path ID or from coordinates, over time or as the visitor scrolls |
terrain | setTerrain, resetTerrain | Raising or lowering the terrain relief at runtime |
appearance | setAppearance | Dimming the base map while your places stay bright |
accent | accent and accentColor on setContext items | A glowing halo on chosen pins |
reveal | reveal on setContext | A staggered pin entrance |
Rules that hold for every story-map feature:
- Nothing changes until you ask. A page that sends none of these commands gets exactly the 2.0.0 map.
- Every command is acked, once it is applied or rejected, and every failure also sends
error { scope, reason, command }. - Every animation has a
durationin whole milliseconds, clamped to its range. - The latest command wins. A new command for the same line, the terrain or the appearance stops the running animation where it is and continues from there; it never jumps back to the start.
- Your settings survive a style reload: lines, a terrain override and a dim are put back.
- Nothing is drawn above your pins. Lines and the dim sit in the map below the pins, donuts, labels and the place card.
- Reduced motion. When the visitor’s system asks for reduced motion (
prefers-reduced-motion: reduce, read inside the iframe), every story-map animation goes straight to its end state, and its completion event still fires. Glow halos don’t breathe and pins appear without the reveal.
if (site.hasCapability('lines')) { site.setLines([ { lineId: 'creek', pathId: 'PATH-ID', color: '#8FD0FF', width: 5, glow: true, progress: 0 }, { lineId: 'iron', coordinates: [[-77.94, 40.86], [-77.85, 40.90]], color: '#C9AE7A', width: 4, dash: false, progress: 0 } ], { fit: false }); site.animateLine('creek', { to: 1, duration: 2600, easing: 'easeInOut' }); // drawn over time site.setLineProgress('iron', 0.42); // drawn by scroll site.clearLines();}| Field | Required | Rule |
|---|---|---|
lineId | Yes | Your own ID: 1–64 characters from A–Z, a–z, 0–9, _ and -, unique in the set. |
pathId or coordinates | Exactly one | pathId is the ID of a trail (a path place) on this map. coordinates is 2–5,000 [lng, lat] pairs of finite numbers, with longitude from −180 to 180 and latitude from −85 to 85. |
color | No | #RRGGBB. Defaults to the map’s primary colour. |
width | No | 1–12 px at zoom 14, default 4. It scales with zoom like the story route: half as wide at zoom 10, twice as wide at zoom 18. |
opacity | No | 0–1, default 0.9. |
dash | No | true draws a dashed line. Default false. A dashed line still animates. |
glow | No | true adds a soft, blurred line underneath, three times as wide. Default false. |
progress | No | How much of the line is drawn, 0–1. Default 1 (fully drawn); 0 is not drawn yet. |
| SDK method | Raw command | What it does |
|---|---|---|
setLines(lines, { fit }) | setLines { lines, fit? } | Replaces the whole set of lines. fit: true fits all the lines drawn (with the same margin as fitBounds, maximum zoom 16). Answers with linesApplied, then ack. |
clearLines() | setLines { lines: [] } | Removes every line. |
animateLine(lineId, { to, from?, duration?, easing? }) | animateLine { lineId, to, from?, duration?, easing? } | Animates the line’s progress to to (0–1), from from (0–1, default the current progress). duration is 0–5000 ms, default 1200. easing is linear, easeInOut (default) or easeOut. Sends lineAnimationEnded when it finishes or is replaced. |
setLineProgress(lineId, at) | setLineProgress { lineId, at } | Sets the progress at once, with no animation, for scroll scrubbing. A change of less than 0.00001 is ignored. It stops a running animateLine on that line. |
Limits. At most 20 lines, and at most 20,000 coordinates once path IDs are resolved. A call over either limit is rejected whole with error { scope: 'line', reason: 'too_large' }. The SDK throws when it can tell beforehand (more than 20 lines, or more than 20,000 coordinates in coordinates lines).
How a path ID is found. The embed looks in the places it has loaded, then in the page context, then fetches it, so a trail doesn’t have to be on screen. All the path IDs of one setLines are fetched together. A trail with several parts is drawn part by part in its own order, and progress runs along their combined length. A line that can’t be drawn never blocks the others: they are drawn, and it is listed in missing.
Progress is measured along the line’s length, and the drawn end stops at an exact point between vertices. Lines are draped on the terrain. They sit above trails and areas and below the story route, so a route drawn over a creek stays readable.
| Event | Payload | When it fires |
|---|---|---|
linesApplied | { lineIds, missing: [{ lineId, reason }] } | After every setLines. lineIds lists the lines drawn. reason is unknown_path (no such place), not_a_path (the place isn’t a trail) or invalid_geometry. |
lineAnimationEnded | { lineId, at, cancelled } | An animateLine finished (cancelled: false) or was replaced by a later command (cancelled: true). at is the progress it reached. |
Terrain
Section titled “Terrain”if (site.hasCapability('terrain')) { site.setTerrain({ exaggeration: 1.8, duration: 1200 }); // animate from the current relief site.resetTerrain({ duration: 800 }); // back to the map style's own value}exaggerationis 0–3, used to two decimal places.durationis 0–3000 ms, default 800.- Answers with
terrainChanged { exaggeration, source }when the animation ends:sourceis'host'aftersetTerrainand'style'afterresetTerrain. - The relief you set stays through style reloads until
resetTerrain. - A map style without terrain rejects terrain commands with
error { scope: 'terrain', reason: 'no_terrain' }(and acks). The embed never adds terrain to a style by itself: terrain costs tiles and frame time, so it is a choice made in the map’s style. - Raising the relief moves the ground under a fixed camera, so on a pitched camera the point you look at can shift slightly.
Appearance (dim)
Section titled “Appearance (dim)”if (site.hasCapability('appearance')) { site.setAppearance({ dim: 0.55, color: '#06120C', duration: 1800 }); site.clearAppearance({ duration: 600 }); // = setAppearance({ dim: 0 })}dimis 0–0.8; above 0.8 the map stops reading as a map.coloris#RRGGBB, default#06120C.durationis 0–3000 ms, default 600.- Raw command:
setAppearance { dim, color?, duration? }.clearAppearance()sendssetAppearance { dim: 0 }. - Answers with
appearanceChanged { dim, color }when the transition ends. - The dim darkens the base map only: its roads, labels and imagery, and the boundary mask. Your areas, trails, sections, lines, story route, landmarks and every pin stay at full brightness. On a pitched camera the sky darkens with it.
- A dim stays through style reloads until you clear it.
Pin accent and reveal
Section titled “Pin accent and reveal”site.setContext([ { poiId: 'NATURE-INN-ID', role: 'featured', accent: 'glow', accentColor: '#F6D58E' }, { poiId: 'PORT-MATILDA-ID', role: 'featured', accent: 'glow' }, { poiId: 'LODGE-ID', role: 'mentioned' }], { mode: 'emphasize', fit: true, reveal: { stagger: 80 } });accent: 'glow'gives the place’s pin a soft halo inaccentColor(default: the map’s primary colour) that breathes slowly, every 2.4 s.'glow'is the only accent so far. The embed removes any other value and still applies the item; the SDK throws on one.- A selected place shows its usual selected ring and pulse, and its halo is hidden while it is selected. A hovered place lifts as usual.
reveal: { stagger }(0–200 ms) makes the places that enter the context in this call pop in one after another, in the order ofitems: each pin scales up from 60% and fades in over 260 ms, startingindex × staggerms after the first. Pins that were already showing don’t animate again. Places after the 20th share the 20th’s start time, so the whole entrance ends within20 × stagger + 260ms.- Without
accentorreveal,setContextbehaves exactly as before. Site embed 2.0.0 ignores both fields and applies the rest of the call.
Story-map errors
Section titled “Story-map errors”Every story-map error is { scope, reason, command }, where command is the message type that failed, and is followed by that command’s ack.
scope | reason | When |
|---|---|---|
line, terrain or appearance | invalid_payload | A raw message broke one of the rules above. The SDK checks first and throws, so SDK hosts don’t see this. |
line | too_large | More than 20 lines, or more than 20,000 coordinates after resolving path IDs. Nothing changes. |
line | unknown_line | animateLine or setLineProgress named a line that isn’t drawn. |
terrain | no_terrain | The map’s style has no terrain. |
The line reasons unknown_path, not_a_path and invalid_geometry aren’t errors: they come in linesApplied.missing. capability_unavailable never comes from the embed: it is the code the SDK rejects with when the embed doesn’t list the feature.
Example: a chapter-driven story page
Section titled “Example: a chapter-driven story page”Each chapter of the page tells the map what to show once the visitor settles on it (debounce your scroll handler, and fly once to the last chapter reached):
const can = (capability) => site.hasCapability(capability);const quiet = () => {}; // cleanup calls may reject on an older embed
function showChapter(chapter, progress) { switch (chapter) { case 'fish': site.setContext(fishItems, { mode: 'emphasize', fit: true, reveal: { stagger: 80 } }); if (can('lines')) { site.setLines([{ lineId: 'creek', pathId: CREEK_PATH_ID, color: '#8FD0FF', glow: true, progress: 0 }]) .then(() => site.animateLine('creek', { to: 1, duration: 2600 })) .catch(quiet); } site.highlightSection(KEYSTONE_SECTION_ID); break; case 'parks': if (can('lines')) site.clearLines().catch(quiet); site.highlightSection(null); site.flyTo({ center: PLATEAU, zoom: 12.6, pitch: 68, bearing: -40, duration: 1600 }); if (can('terrain')) site.setTerrain({ exaggeration: 2, duration: 1400 }).catch(quiet); break; case 'iron': if (can('terrain')) site.resetTerrain({ duration: 600 }).catch(quiet); site.setContext(ironItems, { mode: 'emphasize', fit: true, reveal: { stagger: 80 } }); if (can('lines')) { site.setLines([{ lineId: 'iron', coordinates: [JULIANA, CURTIN], color: '#C9AE7A', progress: 0 }]).catch(quiet); } break; case 'stay': if (can('lines')) site.clearLines().catch(quiet); site.setContext(stayItems.map((item) => ({ ...item, accent: 'glow', accentColor: '#F6D58E' })), { mode: 'emphasize', fit: true, reveal: { stagger: 80 } }); if (can('appearance')) site.setAppearance({ dim: 0.55, duration: 1800 }).catch(quiet); break; }}
// While the visitor scrolls through the iron chapter:function onIronScroll(chapterProgress) { if (can('lines')) site.setLineProgress('iron', chapterProgress).catch(quiet);}
// Leaving the stay chapter, or the page:function leaveStory() { if (can('lines')) site.clearLines().catch(quiet); if (can('terrain')) site.resetTerrain().catch(quiet); if (can('appearance')) site.clearAppearance().catch(quiet);}Behaviours to rely on
Section titled “Behaviours to rely on”These are deliberate, and hosts depend on them:
clearSelection, Escape and the card’s × deselect twice: a peek falls back to its anchor, then that closes. The focus stack stays.focussendsfocusChangedeven when the anchor set didn’t change.hoverechoespoiHoveredorpoiUnhovered. An ID the embed hasn’t loaded counts as an unhover.backon an empty focus stack is acked and sends nothing else.- A
highlightsent while nothing is selected (or, on a wide map, before the card is showing) is undone at once, althoughassociationHighlightedhas already been sent. - Padding passed to
setContextorfitBoundsis kept for later calls, not used once. - Commands sent before the map has loaded are dropped. The SDK queues them until
ready. readyfires on Mapbox’sloadevent, never earlier.- The same
setPaddingtwice does nothing, and a change while the camera moves waits for the move to end. - A
flyToyou send doesn’t load the map’s places for the new area. Fits and the visitor’s own moves do. Hover only finds places that have loaded. lat,lngandztreat0, empty and non-numeric values as unset.bandphonour0.- The default mode is
allfor a plain iframe andemphasizefor the SDK. - A disabled map sends nothing at all, and there is no load-failure event.
- A page place that is a trail or an area shows its pin and draws its line or shape.
onlystill shows landmarks and the boundary mask.- Badges appear only on photo markers.
- When a category’s pin icon fails to load, that category’s pins don’t show.
associationSelectedcomes from the selection itself, beforepoiSelected, whatever caused the selection.ackgoes back to the window that sent the command. Events go to the parent with target origin'*'. Nothing checks origins.boundsChangedcomes 250 ms after a move ends.dimensionChangedis a formula of the width.- An invalid
setContext(a plain ID string, a missingrole) is ignored entirely: nocontextApplied, noack. clusterClickedfires before the embed’s own zoom. Don’t zoom in response.- The story-map commands are always acked. A failure sends
error { scope, reason, command }before theack. - A new story-map command for the same line, the terrain or the appearance continues from the current value; an
animateLineit replaces ends withlineAnimationEnded { cancelled: true }. - Under reduced motion, story-map animations finish at once and still send their completion events.
- A dim never darkens your places, trails, areas, lines or story route, and the embed never adds terrain to a style that has none.
Versioning
Section titled “Versioning”ready.embedVersionis the embed’s own version,'2.1.0'. The SDK exposes it asgetEmbedVersion().ready.capabilitieslists the optional features the embed supports. Check it (SDK:hasCapability(name)) rather than the version number.ready.sdkVersionechoes the SDK version that loaded the map (from thesdk=parameter), ornull.Wander.VERSIONis the SDK’s version andWander.PROTOCOLthe wire protocol,1.- To pin an SDK version with an integrity hash, see Pinning a version.
Host checklist
Section titled “Host checklist”- Mount once and change content around it. Keep the iframe in a layout that survives client-side navigation, and have each page call
setContext(thenselectorfocusif it is about one place). Re-mounting per page throws away the map, its loaded places and its association cache. - Paint the frame before the map paints. Pass
background(SDK) orbg(URL) in your page colour, usefill: truefor full-bleed maps, show a skeleton onloadingand hide it onready, and time out ifreadynever arrives. - Send four-sided padding on every layout change: panel shown or hidden, sheet detent, mini card. Use
setPadding({ top, right, bottom, left }). - Use
contextAppliedas the capability check, notisConnected(), and logmissing: those places aren’t on this map. - Act on
poiSelectedonly whensource === 'user'. Your ownselect,focusandflyTo { select: true }come back withsource: 'host'. setContextandsetModereset the page (focus stack, selection, highlight, external place). Sendselectorfocusafter them, never before.- Route on
associationSelected. The embed only peeks; your page decides whether that becomes a page change. - Let the embed handle cluster clicks. It zooms by itself;
clusterClickedis for your analytics or UI. - Handle
externalPlaceSelectedif your site has a trip or itinerary. The embed only pins the place and reports it. - Feature-detect story-map features with
ready.capabilities(SDK:hasCapability), and catch the SDK’s story-map promises, so the same page works on an embed without them.