Get started

Show a map

Show a map with controls

MapView draws the map. MapControls is the floating stack over it, and every built-in button in that stack is a boolean you switch on. This page is about which control does which job. The Props tab lists all 19 components and every prop they take.

Render the map

MapView constructs the map, authenticates its tile requests, and reports the instance through onReady. Put that instance on MountayaElementsProvider and every control on the page drives the same map.

import { useState } from "react";
import { Mountaya } from "@mountaya/client/browser";
import type { MountayaMap } from "@mountaya/elements";
import { MapView, MountayaElementsProvider } from "@mountaya/elements-react";

const client = new Mountaya({ credential: { publishableKey: "pk_live_…" } });

export const Map = () => {
  const [map, setMap] = useState<MountayaMap | null>(null);

  return (
    <MountayaElementsProvider map={map}>
      <MapView center={[6.86, 45.83]} client={client} onReady={setMap} zoom={12} />
    </MountayaElementsProvider>
  );
};

MapView fills its container, so give that container a height. Options are read once, at construction: a different credential needs a new key, not a changed prop. A session token that rotates is not a different credential: a client whose sessionToken is a function hands the map the token current at each request. terrain picks a base map from the layers manifest. mapStyle takes a complete style document of your own. style is DOM CSS. onReady fires after the style has loaded, which is the first moment the map accepts sources and layers. onDestroy fires for a map onReady handed out, just before that map is destroyed. A map torn down before its style loaded reports neither, which is what React StrictMode does to the first map of every development mount.

onError and onAccessRequired report what no call of yours could reject, and both are attached before the map makes its first request. onAccessRequired carries the status and the address of a 401, a 402 or a 403 on a tile, a sprite, a glyph, a style or the layers manifest. onError carries every other failure. A map that cannot be built at all throws to the nearest error boundary.

transformRequest replaces the tile transform the map derives from its client. Supply one at mount and MapView reads the prop at each request, so a transform rebuilt when a token rotates reaches the next tile. Whether there is one at all is the mount's to say: a transform first supplied on a later render is ignored, and a render that omits the prop keeps the last one supplied. maplibre takes MapLibre's own options: they are applied before the map's, so they add to them and replace none.

Packages and provider setup are on Install the SDK.

Add the control stack

Each built-in control is a boolean flag, off by default. children join the same stack, spacing and focus order.

<MapControls
  compass
  disabled={agentIsDriving}
  geolocate
  layers
  onClosePanel={() => setPanel(null)}
  onTogglePanel={(next) => setPanel((current) => (current === next ? null : next))}
  openPanel={panel}
  pois
  position="top-right"
  preferences
  zoom
/>

openPanel decides who owns the open panel. Leave it unset and the stack owns its own, which is what rendering the built-in panels wants. Set it and the stack only reads it, then asks for a change through onTogglePanel and onClosePanel. disabled is read once and threaded to every control in the stack.

Every control is published twice, on the stack and flat: MapControls.Zoom and ZoomControl are the same component.

Pick the control for the job

You want toReach for
Zoom in and outzoom, or ZoomControl
Reset the bearing to northcompass, or CompassControl
Center on the reader's positiongeolocate, or GeolocateControl
Switch terrain and overlayslayers, which opens LayersPanel
Toggle what the POI overlay drawspois, which opens PoiPanel
Change units and map languagepreferences, which opens PreferencesPanel
Open a floating panel of your ownPanelControl
Search places, itineraries and zonesSearchControl
Look around a streetStreetViewControl
Export what the map showsDownloadControl
Open directions for what the map showsDirectionsControl
Add a button that matches the built-insControlButton

CompassRose is the needle CompassControl draws, published so a compass of your own can reuse it. StudioControls belongs to the studio chrome rather than to this package.

Search places, itineraries and zones

SearchControl sits beside the stack rather than in it, because it is a panel-width bar rather than a square chip. It runs Mountaya's unified search through the client on the provider, and renders one list across every kind of result.

<SearchControl
  limit={5}
  onQueryChange={setQuery}
  onSelect={focusResult}
  query={query}
  targets={["itineraries"]}
/>;

query and viewportScope are controlled independently. Pass one without its on* partner to freeze it, which pins a search to a single scope on purpose.

Look around with Street View

StreetViewControl is the handle a reader drags onto the map. It shows the coverage layer while the drag is live, so a reader can see where imagery exists. The imagery is Google's, so this control needs @googlemaps/js-api-loader and a Google Maps API key. Both are optional peers, which a consumer who never mounts the control never installs.

<MapControls zoom>
  <MapControls.StreetView
    bounds={streetViewBounds}
    googleMapsApiKey={googleMapsApiKey}
    minZoom={14}
    onOpen={(lngLat) => openPanorama(lngLat)}
    onUnavailable={notify}
  />
</MapControls>;

Pass exactly one of onOpen and onDrop. The props are a union, so passing both is a type error rather than a precedence question. onOpen keeps the default pipeline (unproject, corridor gate, coverage check) and takes the point that survived it. onDrop replaces that pipeline and answers for the drop itself; return false to bounce the handle. Coverage is a per-point request to Google, and bounds and minZoom are the cheap gates in front of it.

Choose terrains, points of interest and units

Three panels open from the stack, and each takes its options through a prop named after it.

<MapControls
  layers
  layersPanel={{
    isLocked: (slug) => !viewerPlan.terrains.includes(slug),
    onLockedClick: openUpgrade,
  }}
  poiPanel={{ minZoom: 12, showAllToggle: true }}
  pois
  preferences
  preferencesPanel={{ onPreferencesChange: setPreferences, preferences }}
  zoom
/>

LayersPanel picks the terrain and the overlays above it. isLocked is a display concern only, because the Tile API answers 403 whatever a picker shows. It exists so an application that knows the viewer's agreement can say so before the press.

PoiPanel toggles what the points-of-interest overlay draws. Pass groups to stand one row for several manifest keys, under your own label and glyph. Pass keys to show fewer built-in rows.

PreferencesPanel requires onPreferencesChange. A profile row, a cookie and a store are all reasonable homes for a reader's units. The panel picks none of them. Seed from defaultUnits (from @mountaya/elements/profile): every field of UnitFormatOptions is required, and a partial value round-tripped through storage reverts a reader's units without saying so.

Mount a panel on its own when it belongs in a sidebar or a settings drawer.

Set a handler and the default stops firing

Left unset, a control drives the map itself. Set an on* handler and the control becomes presentational: yours decides, and the built-in behaviour never runs. The two never both fire.

<ZoomControl />;                        // zooms the map
<ZoomControl onZoomIn={bus.zoomIn} />;  // calls you, and does not zoom

The two halves of the zoom pair are gated separately. A stack that overrides zoom-in keeps no live zoom-out whose default it never wired. The handlers that report are additive instead, and are named for it: onReady, onError, onAccessRequired, onLockedClick and onUnavailable report something the element still handles.

Two controls require their handler

DownloadControl and DirectionsControl take a required onClick, because they have nothing to drive. Exporting an itinerary and opening directions both end in a surface that belongs to your application. Neither can be guessed. Without a handler they would look live and do nothing.

<MapControls compass zoom>
  <DownloadControl onClick={() => setExportOpen(true)} />
  <DirectionsControl onClick={() => setDirectionsOpen(true)} />
</MapControls>

Both stay live while the map is still loading. They act on an itinerary you already hold, not on something read off the canvas.

Build a control of your own

Five primitives are published so a control you add is indistinguishable from the ones Mountaya ships.

  • ControlButton is the square chip.
  • ControlGroup joins adjacent chips into one raised surface.
  • ControlTooltip draws the control's name and the dimmed line under it.
  • Glyph renders one icon from the provider's set.
  • OptionRow is a selectable line in a panel.
import { ControlButton, MapControls } from "@mountaya/elements-react";

<MapControls compass zoom>
  <ControlButton glyph="download" label="Print" onClick={() => window.print()} />
</MapControls>;

Icons, strings and theme come from the provider, so your control wears the same vocabulary as the built-ins. Customize the elements covers all three.

Next steps