@maptiler/sdk
v3.11.1
Published
The Javascript & TypeScript map SDK tailored for MapTiler Cloud
Readme
MapTiler SDK JS
The MapTiler SDK JS extends MapLibre GL JS, exposes all its features, and adds new ones on top. The SDK is designed to work with the well-established MapTiler service, which provides all the data required to fuel a complete web mapping experience: vector tiles, satellite raster tiles, DEM with Terrain RGB, custom styles with an editor, etc.
Why are we creating a new SDK? To make things simpler for developers working in the MapTiler ecosystem! With MapTiler SDK JS, there is no need to load external plugins for the most basic things, copy-paste complex data source URLs, or look up the syntax to enable 3D terrain every time you start a project. All this is built-in, loaded when needed, or exposed with simple functions. Under the hood, this SDK is opinionated as it's being fed by MapTiler service data, but its MapLibre core makes it 100% compatible with other sources.
In addition, the MapTiler SDK JS provides well-documented and easy-to-use wrapper functions to the MapTiler API services such as: geocoding, static maps, geolocation, as well as a search engine for coordinate reference systems and transforming coordinates from one CRS to another.
📣 Note: If you need only the API Client library to use in a headless fashion and without any map display, check out MapTiler Client JS library for browser and NodeJS.
📖 Documentation 📦 NPM Package 🌐 Website 🔑 Get API Key
📦 Installation
npm install --save @maptiler/sdk⚠️ Please keep in mind that if you use any additional MapTiler modules, you must update them to a version that supports MapTiler SDK JS v3.
From CDN and using the UMD bundle, in the <head></head> section of your HTML file:
<script src="https://cdn.maptiler.com/maptiler-sdk-js/<VERSION>/maptiler-sdk.umd.min.js"></script>
<link href="https://cdn.maptiler.com/maptiler-sdk-js/<VERSION>/maptiler-sdk.css" rel="stylesheet" />🚀 Basic Usage
With ES modules
Recommended for: advanced applications
import { config, Map } from "@maptiler/sdk";
// Add your MapTiler Cloud API key to the config
// (Go to https://cloud.maptiler.com/account/keys/ to get one for free!)
config.apiKey = "YOUR_API_KEY";
// Let's say you have a DIV ready to receive a map
const mapContainer = document.getElementById("my-container-div");
// Instantiate the map
const map = new Map({
container: mapContainer,
});Alternatively, the apiKey can be set as Map option instead of in the config object. Yet, this will still internally propagate to the config object:
import { Map } from "@maptiler/sdk";
// Let's say you have a DIV ready to receive a map
const mapContainer = document.getElementById("my-container-div");
// Instantiate the map
const map = new Map({
container: mapContainer,
apiKey: "YOUR_API_KEY",
});By default, the map will be initialized with the style streets-v2.
Depending on the framework and environment you are using for your application, you will have to also include the CSS file.
For example, with a NextJS app, this can take place at the top of the file _app.ts/js:
import "@maptiler/sdk/dist/maptiler-sdk.css";TypeScript
The SDK is fully typed, but it may happen that types defined in Maplibre GL JS are not visible in your project. This is a known issue that comes from Maplibre being a CommonJS bundle.
There are mainly two ways to address this issue and access to the complete type definition.
- With
esModuleInterop
Set the following in your tsconfig.json:
{
"compilerOptions": {
// ...
"esModuleInterop": true,
}
}- With
moduleResolution
Set the following in your tsconfig.json:
{
"compilerOptions": {
// ...
"moduleResolution": "Bundler",
}
}Note that this second option is not always possible as some frameworks and other dependencies won't let you use the "Bundler" mode.
With CDN
The SDK hosted on our CDN is bundled as Universal Module Definition (UMD) to make it standalone and contain all its dependencies. The CDN also serves the style sheet (CSS).
Recommended for: simple map integration example and demos
<html>
<head>
<title>MapTiler JS SDK example</title>
<style>
html,
body {
margin: 0;
}
#map-container {
position: absolute;
width: 100vw;
height: 100vh;
}
</style>
<!-- Load the SDK CSS -->
<link rel="stylesheet" href="dist/maptiler-sdk.css" />
</head>
<body>
<div id="map-container"></div>
<script src="dist/maptiler-sdk.umd.min.js"></script>
<script>
// Add your MapTiler API key to the config
// (Go to https://cloud.maptiler.com/account/keys/ to get one for free!)
maptilersdk.config.apiKey = "YOUR_API_KEY";
const mapContainer = document.getElementById("my-container-div");
const map = new maptilersdk.Map({
container: mapContainer,
style: maptilersdk.MapStyle.STREETS_DARK,
hash: true,
});
</script>
</body>
</html>Check out the minimalist code samples in the demos directory.
💡 Related Examples
Check out more than 200 examples and tutorials
📘 API Reference
In addition to the details and examples provided in this readme, check out the complete API documentation
Many styles to choose from
MapTiler teams maintain a few styles that we have decided to expose from the SDK. This has two advantages:
- they are easier to remember, no need to type along style URL
- if we make an update to a style, you will benefit from it without modifying your codebase
Here is how it works:
import { Map, MapStyle } from "@maptiler/sdk";
// When instanciating a map
const map = new Map({
container: document.getElementById("my-container-div"),
style: MapStyle.OUTDOOR, // <-- the shorthand for the outdoor style
});
// Or later on, updating the style
map.setStyle(MapStyle.STREETS.DARK);The styles with a shorthand provided by the SDK are the following:
| ID | Screenshot | Comment |
| :---------------------: | :----------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------: |
| MapStyle.STREETS |
| The classic default style, perfect for urban areas.Also available in dark and light mode. |
| MapStyle.DATAVIZ.DARK |
| A minimalist style for data visualization.Also available in color and light mode |
| MapStyle.SATELLITE |
| Only high resolution satellite raster tiles without any labels |
| MapStyle.HYBRID |
| Satellite tile with labels, landmarks, roads ways and political borders |
| MapStyle.OUTDOOR |
| A solid hiking companion, with peaks, parks, isolines and more |
| MapStyle.BASIC |
| A minimalist alternative to STREETS, with a touch of flat design.Also available in dark and light and pastel mode. |
MapTiler provides some reference styles as well as some variants for each. A reference style sets some guidelines about what kind of information is displayed, the granularity of the information, and more generally defines a purpose for which this style is the most relevant: street navigation, outdoor adventure, minimalist dashboard, etc. Then, each reference style offers a range of variants that contain the same level of information and have the same purpose but use different color schemes.
Here is the full list:
MapStyle.STREETS, reference style for navigation and city explorationMapStyle.STREETS.DARK(variant)MapStyle.STREETS.LIGHT(variant)MapStyle.STREETS.PASTEL(variant)
MapStyle.SATELLITEreference style satellite and airborne imagery (no variants)MapStyle.HYBRIDreference style satellite and airborne imagery with labels (no variants)MapStyle.OUTDOORreference style for adventureMapStyle.WINTERreference style for winter adventureMapStyle.DATAVIZ, the perfect style for data visualization, with very little noiseMapStyle.DATAVIZ.DARK(variant)MapStyle.DATAVIZ.LIGHT(variant)
MapStyle.BACKDROP, great style for data visualization when hillshading matters!MapStyle.BACKDROP.DARK(variant)MapStyle.BACKDROP.LIGHT(variant)
MapStyle.BASICreference style for minimalist design and general purposeMapStyle.BASIC.DARK(variant)MapStyle.BASIC.LIGHT(variant)
MapStyle.BRIGHTreference style for high-contrast navigationMapStyle.BRIGHT.DARK(variant)MapStyle.BRIGHT.LIGHT(variant)MapStyle.BRIGHT.PASTEL(variant)
MapStyle.TOPOreference style for topographic studyMapStyle.TOPO.SHINY(variant)MapStyle.TOPO.PASTEL(variant)MapStyle.TOPO.TOPOGRAPHIQUE(variant)
MapStyle.VOYAGERreference style for stylish yet minimalist mapsMapStyle.VOYAGER.DARK(variant)MapStyle.VOYAGER.LIGHT(variant)MapStyle.VOYAGER.VINTAGE(variant)
MapStyle.TONERreference style for very high contrast stylish mapsMapStyle.TONER.BACKGROUND(variant)MapStyle.TONER.LITE(variant)MapStyle.TONER.LINES(variant)
MapStyle.OCEANreference style with bathymetric highlights, does not have any variants.MapStyle.LANDSCAPEreference style terrain map for data overlays and visualisationsMapStyle.LANDSCAPE.DARK(variant)MapStyle.LANDSCAPE.VIVID(variant)
MapStyle.AQUARELLEreference style watercolor map for creative useMapStyle.AQUARELLE.DARK(variant)MapStyle.AQUARELLE.VIVID(variant)
MapStyle.OPENSTREETMAP(reference style, this one does not have any variants)
All reference styles (instances of ReferenceMapStyle) and style variants (instances of MapStyleVariant) have methods to know the alternative styles and variants that belong to the same reference style (.getVariants()). This is handy to provide a default/dark/light alternative color scheme, yet preserving the same level of details as in the reference style. Read more about about ReferenceMapStyle and MapStyleVariant.
You can also use classic styles with just a string if you know their MapTiler ID:
map.setStyle("outdoor-v2");And finally, you can use your own custom styles designed with our style editor. Every custom style is given a unique ID, for instance: c912ffc8-2360-487a-973b-59d037fb15b8.
This ID can be provided as such:
map.setStyle("c912ffc8-2360-487a-973b-59d037fb15b8");Or in its extended form:
map.setStyle("https://api.maptiler.com/maps/c912ffc8-2360-487a-973b-59d037fb15b8/style.json");
// this could be suffixed with the API token as wellAnd can even be provided in the URI form:
map.setStyle("maptiler://c912ffc8-2360-487a-973b-59d037fb15b8");Globe or Mercator projection?
The Web Mercator projection (Wikipedia) has been the go-to standard in cartography since the early days or web mapping. Partly for technical reasons but also because it is great for navigation as well as for showing the entire world in one screen, with no hidden face. That being said, Mercator's heavy distorsion at high latitudes, as well a the discontinuity at the poles can be a limitation for data visualization and has been critisized for providing a biased view of the world.
The globe projection, available starting from MapTiler SDK v3, does not suffer from these biases and can feel overall more playfull than Mercator. It can be a great choice for semi-global data visualization, especially for data close to the poles, thanks to its geographic continuity.
| Mercator projection | Globe projection |
| :----------------------------------------------: | :-------------------------------------------: |
|
|
|
The choice between Mercator and Globe can be done at different levels and moments in the lifecycle of the map, yet, unless stated otherwise, Mercator remains the default.
In the style, using the
projectiontop-level property.
This projection is used when the style loads unless it has been overriden by one of the options below.{ "version": ..., "id": ..., "name": ..., "sources": ..., "layers": ..., // Make the style use the globe projection at load time "projection": { "type": "globe" } // ... } // or { "version": ..., "id": ..., "name": ..., "sources": ..., "layers": ..., // Make the style use the mercator projection at load time "projection": { "type": "mercator" } // ... }In the constructor of the
Mapclass, using theprojectionoption.
This overrides theprojectionproperty from the style (if any) and will persist it if the map style changes later, even if the style contains its own projection.const map = new maptilersdk.Map({ container: "map", projection: "globe", // Force a globe projection }); // or const map = new maptilersdk.Map({ container: "map", projection: "mercator", // Force a mercator projection });Using the built-in methods.
This changes the projection for the current style and optionally also persists it if the map style changes later.map.setProjection("mercator", { persist: true }); // or map.setProjection("globe", { persist: true });ℹ️ Deprecated methods
enableGlobeProjection()andenableMercatorProjection()also persist the projection.Without persistance, when the style next changes, the projection is reset to a previously persisted projection, or a projection specified in the
Mapoptions, or a projection specified inprojectionproperty of the style itself, or the default (Mercator) projection.map.setProjection("mercator"); // or map.setProjection("globe");The signature built in Maplibre GL JS is also usable, however a style specification other than
"mercator"or"globe"can't be persisted.map.setProjection({ type: "mercator" }); // or map.setProjection({ type: "globe" }); // or map.setProjection({ type: ["step", ["zoom"], "vertical-perspective", 3, "mercator"] });Using the
MaptilerProjectionControl. Not mounted by default, it can easily be added with a single option in theMapconstructor:const map = new maptilersdk.Map({ container: "map", projectionControl: true, // or the position such as "top-left", "top-right", }); // "bottom-right" or "bottom-left"This dedicated control will show a globe icon to transition from Mercator to globe projection and will show a flat map icon to transition from globe to Mercator projection. The chosen projection persist with future style changes.
You can also forget the persisted projection. This is the only way how to make projection specified inside style itself work again if
Mapconstructorprojectionoption was specified, ormap.setProjection(..., { persist: true })was called, orMaptilerProjectionControlwas used, starting from the next style change.map.forgetPersistedProjection();
Field of view (FOV)
The internal camera has a default vertical field of view (wikipedia) of a wide ~36.86 degrees. In globe mode, such a large FOV reduces the amount of the Earth that can be seen at once and exaggerates the central part, comparably to a fisheye lens. In many cases, a narrower FOV is preferable. Here is how to update if:
// Ajust de FOV, with values from 1 to 50
map.setVerticalFieldOfView(10);📣 Note: with the Mercator projection, it is possible to set a FOV of
0, which yields a true orthographic projection (wikipedia), but the globe projection does not allow this.
Here is a table of FOV comparison:
| 01° | 10° | 20° | 30° | 40° | 50° |
| :--------: | :-------: |:-------: |:-------: |:-------: |:-------: |
|
|
|
|
|
|
|
Globe screenshots

📣 Note: Terrain is not fully compatible with the globe projection yet so it's better to disable it at low zoom level (from afar) and to choose the Mercator projection at higher zoom level (from up close).
Centering the map on visitors
It is sometimes handy to center the map on the visitor's location, and there are multiple ways of doing it but for the SDK, we have decided to make this extra simple by using the IP geolocation API provided by MapTiler Geolocation API, directly exposed as a single option of the Map constructor. There are two strategies:
POINT: centering the map on the actual visitor location, optionally using thezoomoption (zoom level13if none is provided). As a more precise option, if the user has previously granted access to the browser location (more precise) then this is going to be used.COUNTRY: fitting the map view on the bounding box of the visitor's country. In this case, thezoomoption, if provided, will be ignored
Here is how the map gets centered on the visitor's location:
new maptilersdk.Map({
// ... other options
geolocate: maptilersdk.GeolocationType.POINT,
});Here is how the map fits the visitor's country bounds:
new maptilersdk.Map({
// ... other options
geolocate: maptilersdk.GeolocationType.COUNTRY,
});The geolocation options will not be taken into consideration in the following cases:
- if the
centeroption is provided, then it prevails - if the
hashoption is provided with the valuetrueAND a location hash is already part of the URL. Ifhashistruebut there is not yet a location hash in the URL, then the geolocation will work.
📣 Note: if none of the options
centerorhashis provided to theMapconstructor, then the map will be centered using thePOINTstrategy, unless thegeolocatehas the valuefalse.
📣 Note 2: the term IP geolocation refers to finding the physical location of a computer using its IP address. The IP address is a numerical identifier of a computer within a network, just like the phone number for a telephone. The IP geolocation is not using the GPS of a device and usually provides a precision in the order of a few hundred meters. This precision may vary based on many local parameters such as the density of the network grid or the terrain, this is why it is generally better not to use a zoom level higher than
14.
Easy to add controls
The term "control" is commonly used for all sorts of buttons and information displays that take place in one of the corners of the map area. The most well-known are probably the [+] and [-] zoom buttons as well as the attribution information. Plenty of others are possible and we have made a few easy to add and directly accessible from the Map constructor options:
navigationControl- Shows the
[+],[-]and tilt/bearing/compass buttons - a boolean or a corner position. Showing on the top-right by default. Hidden if
false.
- Shows the
geolocateControl- Shows an arrow-shaped locate button. When clicked, it adds a marker and centers the map. If clicked again, the marker disappears (unless the map was moved since first clicked)
- a boolean or a corner position. Showing on the top-right by default. Hidden if
false.
terrainControl- Shows a button to enable/disable the 3D terrain (does not tilt the map)
- a boolean or a corner position. Hidden by default, showing on top-right if
true.
scaleControl- Shows a distance scale. The unit (
"metric","imperial"or"nautical") can be set in the config objectconfig.unit(default:"metric") - a boolean or a corner position. Hidden by default, showing on bottom-right if
true.
- Shows a distance scale. The unit (
fullscreenControl- Shows a button that toggles the map into fullscreen
- a boolean or a corner position. Hidden by default, showing on top-right if
true.
The corner positions possible are:
"top-left""top-right""bottom-left""bottom-right"
Example:
import { Map } from "@maptiler/sdk";
const map = new Map({
container: document.getElementById("my-container-div"),
terrainControl: false,
scaleControl: true,
fullscreenControl: "top-left",
});🧩 Custom Controls
MapTiler SDK JS supports two flexible ways to add custom controls to your map interface. Whether you're building a dynamic application or integrating with static HTML, there's a matching approach for both.
Programmatic Controls
Use map.addControl() with MaptilerExternalControl to register custom UI elements manually. This approach is ideal for dynamic logic, event-driven behavior, or integration with frameworks like React. The control element can be provided either as a reference to the element itself, or as its CSS selector. Optionally, two callback functions can be provided:
onClickfunction that is called when the element is clicked, andonRenderfunction that is called every time the map renders a new state.
Both callbacks receive the active Map instance, the associated control element itself, and an event object associated with the original event (PointerEvent and MapLibreEvent respectively).
Example
const panControl = new maptilersdk.MaptilerExternalControl(
".pan-control",
(map) => map.panBy([10, 10]), // Move southeast on click
(map, el) =>
el.classList.toggle(
// Style based on hemisphere
"northern-hemisphere",
map.getCenter().lat > 0,
),
);
map.addControl(panControl);
const element = document.createElement("button");
element.textContent = "Pan NW";
map.addControl(
new maptilersdk.MaptilerExternalControl(
element,
(map) => map.panBy([-10, -10]), // Move northwest
),
"top-left",
);Behavior Overview
- On add, control element is moved into the map UI
onClickbinds user interactiononRenderenables state-based styling or logic- Control maintains its own DOM context
- On removal, element is returned to its original DOM position (if any) to not interfere with DOM handling of frameworks like React
Declarative Controls
Add controls using HTML attributes – no JavaScript required. This is perfect for simple UI setups.
Enabling Detection
const map = new maptilersdk.Map({
container: "map",
customControls: true, // or ".custom-ui"
});You can pass true to enable detection globally, or a CSS selector to scope it.
Declaring Controls
Use data-maptiler-control attribute:
<button data-maptiler-control="zoom-in">+</button>Supported values:
| Value | Description |
| ------------------- | ------------------------------------------------ |
| zoom-in | Zooms in |
| zoom-out | Zooms out |
| toggle-projection | Switches Mercator ↔ Globe |
| toggle-terrain | Toggles terrain layer |
| reset-view | Resets bearing, pitch, and roll |
| reset-bearing | Resets bearing only |
| reset-pitch | Resets pitch only |
| reset-roll | Resets roll only |
| (empty) | Registers control without built-in functionality |
⚠️ An error is thrown if an unrecognized value is used.
Grouping Controls
Use data-maptiler-control-group to group buttons:
<div data-maptiler-control-group>
<button data-maptiler-control="zoom-in">+</button>
<button data-maptiler-control="zoom-out">−</button>
</div>Groups are styled and positioned together but don't add functionality themselves. Functional behavior is attached to valid descendant elements.
Positioning Controls
Use data-maptiler-position to set placement:
<button data-maptiler-control="reset-view" data-maptiler-position="top-left">↻</button>
<div data-maptiler-control-group data-maptiler-position="bottom-right">
<button data-maptiler-control="zoom-in">+</button>
<button data-maptiler-control="zoom-out">−</button>
</div>Valid positions: 'top-left', 'top-right', 'bottom-left', 'bottom-right'
Styling with CSS Variables
When declarative controls are enabled, the map container exposes dynamic CSS variables:
| Variable | Description | Type |
| -------------------------------- | ------------------------------------------------ | --------------- |
| --maptiler-center-lng | Longitude of center | unitless number |
| --maptiler-center-lat | Latitude of center | unitless number |
| --maptiler-zoom | Zoom level | unitless number |
| --maptiler-bearing | Map rotation | unitless number |
| --maptiler-pitch | Pitch angle | unitless number |
| --maptiler-roll | Roll angle | unitless number |
| --maptiler-is-globe-projection | true if globe is activefalse otherwise | string |
| --maptiler-has-terrain | true if terrain is activefalse otherwise | string |
Example
.compass-icon {
transform: rotateX(calc(var(--maptiler-pitch) * 1deg)) rotateZ(calc(var(--maptiler-bearing) * -1deg));
}
@container style(--maptiler-is-globe-projection: true) {
.projection-icon {
content: "globe";
}
}3D terrain in one call
Do you want to enable 3D terrain? That's easy now with a single function call:
// With the default exaggeration factor of 1
map.enableTerrain();
// Or, if you want to boost some volume a little
map.enableTerrain(1.5);The terrain can also be enabled directly from the Map constructor, with the options terrain (a boolean, false by default) and terrainExaggeration (a number, 1 by default):
const map = new Map({
// some options...
terrain: true,
terrainExaggeration: 1.5,
});At any point, you can modify the exaggeration factor:
map.setTerrainExaggeration(2);Or simply disable it:
map.disableTerrain();📣 Note: Keep in mind that setting an exaggeration factor at
0will result in the same result as disabling the elevation but that terrain RGB tiles will still be fetched in the background.
📣 Note 2: please be aware that due to the volume and elevation of the map floor in 3D space, the navigation with the terrain enabled is slightly different than without.
By default, enabling, disabling or even just updating the terrain exaggeration will result in a 1-second animation. This is possible to modify with the following Map method:
// Duration in milliseconds
map.setTerrainAnimationDuration(500);Terrain events
"terrain"event
As an extension of Maplibre GL JS, MapTiler SDK is also exposing the terrain event "terrain". This event is triggered when a terrain source is added or removed:
map.on("terrain", (e) => {
// your logic here
});Since MapTiler SDK adds animation and the terrain data is necessary all along, the "terrain" event will be called at the very beginning of the terrain animation when enabling and at the very end when disabling.
"terrainAnimationStart"and"terrainAnimationStop"events
With the animation of the terrain, it can sometimes be convenient to know when the animation starts and ends. These two events are made just for that, here are how they work:
map.on("terrainAnimationStart", (event) => {
console.log("Terrain animation is starting...");
});
map.on("terrainAnimationStop", (event) => {
console.log("Terrain animation is finished");
});The event argument is an object that contains (among other things) a terrain attribute. In the case of "terrainAnimationStop", this terrain attribute is null if the animation was about disabling the terrain, otherwise, this is just a propagation of map.terrain.
In the following example, we decide to associate the terrain animation with a change of camera, e.g. from clicking on the terrain control:
- when the terrain is enabled, it pops up with an animation and only then the camera is animated to take a lower point of view
- when the terrain is disabled, it is flattened with an animation and only then the camera is animated to a top view
map.on("terrainAnimationStop", (e) => {
map.easeTo({
pitch: e.terrain ? 60 : 0,
duration: 500,
});
});Halo and Space Options
The halo and space options allow for enhanced visual customization of the map, especially for globe projections.
halo (Atmospheric Glow)
The halo option adds a gradient-based atmospheric glow around the globe, simulating the visual effect of Earth's atmosphere when viewed from space.
Usage:
You can enable a simple halo by setting it to true:
const map = new maptilersdk.Map({
container: document.getElementById("map"),
style: maptilersdk.MapStyle.OUTDOOR,
halo: true,
});For more customization, you can define a radial gradient with scale and stops:
const map = new maptilersdk.Map({
container: document.getElementById("map"),
style: maptilersdk.MapStyle.OUTDOOR,
halo: {
scale: 1.5, // Controls the halo size
stops: [
[0.2, "transparent"],
[0.2, "red"],
[0.4, "red"],
[0.4, "transparent"],
[0.6, "transparent"],
[0.6, "red"],
[0.8, "red"],
[0.8, "transparent"],
[1.0, "transparent"],
],
},
});You can also set the halo dynamically after the map loads:
map.on("load", () => {
map.setHalo({
scale: 2,
stops: [
[0.0, "rgba(135,206,250,1)"],
[0.5, "rgba(0,0,250,0.75)"],
[0.75, "rgba(250,0,0,0.0)"],
],
});
});To disable state transitions for halo or space:
map.disableHaloAnimations();
map.disableSpaceAnimations();space (Background Environment)
The space option allows customizing the background environment of the globe, simulating deep space or skybox effects.
Usage
You can enable a simple space background with a solid color:
const map = new maptilersdk.Map({
container: document.getElementById("map"),
style: maptilersdk.MapStyle.OUTDOOR,
space: {
color: "#111122", // Dark space-like color
},
});Alternatively, you can provide a cubemap for a space backround using one of the following methods:
Predefined Presets:
space: Dark blue hsl(210, 100%, 4%) background and white stars (transparent background image). Space color changes the background color, stars always stay white.stars(default): Black background (image mask), space color changes the stars color, background always stays black.milkyway: Black half-transparent background with standard milkyway and stars. Space color changes the stars and milkyway color, background always stays black.milkyway-subtle: Black half-transparent background with subtle milkyway and less stars. Space color changes the stars and milkyway color, background always stays black.Black half-transparent background with standard milkyway and stars. Space color changes the stars and milkyway color, background always stays black.milkyway-bright: Black half-transparent background with bright milkyway and more stars. Space color changes the stars and milkyway color, background always stays black.milkyway-colored: Full background image with natural space colors. Space color doesn’t change anything (non transparent image).
const map = new maptilersdk.Map({
container: document.getElementById("map"),
style: maptilersdk.MapStyle.OUTDOOR,
space: {
preset: "space",
},
});Cubemap Images (Custom Skybox):
const map = new maptilersdk.Map({
container: document.getElementById("map"),
style: maptilersdk.MapStyle.OUTDOOR,
space: {
faces: {
nX: "/path-to-image/nX.png",
nY: "/path-to-image/nY.png",
nZ: "/path-to-image/nZ.png",
pX: "/path-to-image/pX.png",
pY: "/path-to-image/pY.png",
pZ: "/path-to-image/pZ.png",
},
},
});Cubemap Path with image format
This fetches all images from a path, this assumes all files are named px, nx, py, ny, pz, nz and suffixed with the appropriate extension specified in format.
const map = new maptilersdk.Map({
container: document.getElementById("map"),
style: maptilersdk.MapStyle.OUTDOOR,
space: {
path: {
baseUrl: "spacebox/transparent",
format: "png", // Defaults to PNG
},
},
});Set the space background dynamically:
map.on("load", () => {
map.setSpace({
color: "red",
path: {
baseUrl: "spacebox/transparent",
},
});
});Note: if space.color or space.<faces | path | preset> are not explicitly set in the call to setSpace, then the previous value will remain for this field.
Further code examples can be found in ~/demos/
ImageViewer
MapTiler's ImageViewer component allows you to display tiled, non-georeferenced images but interact with them in almost the same way you would if you were displaying map. These can be handy for zoomable non-georeferenced, geographically "inaccurate" maps such as hotel maps, golf courses, theme parks etc. Think pixels instead of lattitudes and longtidues.
export type ImageViewerConstructorOptions = {
imageUUID: string; // the unique UUID of the image object you are displaying
center?: [number, number]; // the center you want the viewer to init on, defaults to the center of the image.
container: string | HTMLElement // the container element you want to mount the viewer on
apiKey: string; // your MapTiler API Key
zoom?: number;
maxZoom?: number;
minZoom?: number;
bearing?: number;
debug?: boolean; // whether you want to debug the tiles
};
const imageViewer = new ImageViewer({
container: document.getElementById("map")!, // the container element you want to use
apiKey: "YOUR_MAPTILER_API_KEY", // your api key
imageUUID: "11111111-2222-3333-4444-555555555555", // unique UUID of the image object
// ...other options, see below
}: ImageViewerConstructorOptions);
await imageViewer.onReadyAsync()
// OR
imageViewer.on("imageviewerready", () => { console.log('Ready!') })Methods
ImageViewer provides a subset of methods for interaction with the map. A major caveat is that the ImageViewer component works in pixels and not in LngLat. Thus, when using methods such as setCenter or flyTo the pixel values provided refer to the absolute pixel position on the image, not screen pixel position.
Imagine your image is 10,000px x 10,000px, if regardless if your zoom is 2 or 4, calling .setCenter(500,500) will always position the viewer over the same part of the image.
For full details on supported methods see the type declaration for ImageViewer or visit the (docs)[https://docs.maptiler.com/sdk-js/examples/image-viewer/].
Events
In a similar manner, a subset of map events are fired by the image viewer. All UI interaction events that would normally include a LngLat in the event data instead receive an imageX and imageY field, representing an absolute pixel position of the image. This is same for flyTo, jumpTo, panTo etc.
A full list of supported events can be found in the exported type declaration ImageViewerEventTypes
Markers with ImageViewerMarker
An ImageViewerMarker can also be added and used like the usual Markerclass, the main difference being it operates in image pixels, not LngLat coordinates.
const marker = new ImageViewerMarker({ draggable: true });
marker
.setPosition([100, 100]) // position in image pixels.
.addTo(imageViewer)
.on("dragend", (e) => {
console.log("e.target.isWithinImageBounds()", e.target.isWithinImageBounds());
});Full API documentation can be found in the typedocs: npm run doc
Easy language switching
The language generally depends on the style but we made it possible to easily set and update from a built-in list of languages.
The built-in list of supported languages is accessible from the Language object:
import { Language } from "@maptiler/sdk";In the UMD bundle, it will be directly at maptilersdk.Language.
There are three distinct ways to set the language of a map:
- Global way, using the config object:
import { config } from "@maptiler/sdk";
config.primaryLanguage = Language.ENGLISH;Then, if any further language setting is applied, all the map instances created afterward will use this language.
- Set the language at instantiation time:
const map = new Map({
// some options...
language: Language.ENGLISH, // the ISO codes can also be used (eg. "en")
});It will only apply ENGLISH as the language of this specific map instance (and will not alter the global config).
- Set the language after the map has been instantiated:
map.setLanguage(Language.ENGLISH);Again, it will only apply ENGLISH as the language of this specific map instance (and will not alter the global config).
The list of supported languages is built-in and can be found here. In addition, there are special language flags:
Language.AUTO[DEFAULT] uses the language defined in the web browserLanguage.STYLE_LOCKto strictly use the language defined in the style. Prevents any further language updateLanguage.LOCALuses the language local to each countryLanguage.LATINuses a default with Latin charactersLanguage.NON_LATINuses a default with non-Latin characters
Whenever a label is not supported in the defined language, it falls back to Language.LOCAL.
Here is a sample of some compatible languages:

Built-in support for right-to-left languages
Languages that are written right-to-left such as Arabic and Hebrew are fully supported by default. No need to install any plugins!
If you wish to opt of applying the rtl plugin or wish to use a different compatible rtl text plugin, you can pass the rtlTextPlugin
constructor option as either false (disable the rtl plugin) or a url to load a different plugin.
Note: Once the rtlTextPlugin has been installed once, it cannot be unset nor updated on the current instance. Calling setRTLTextPlugin without setting rtlTextPlugin to false in the constuctor will result in an error.
Visitor language modes
The visitor language modes are special built-in modes made to display labels in two different languages, concatenated when available:
Language.VISITORconcatenates labels in the language of your system and the local languageLanguage.VISITOR_ENGLISHconcatenates labels in English and the local language
const map = new Map({
// some options...
language: Language.VISITOR,
});
// or
const map = new Map({
// some options...
language: Language.VISITOR_ENGLISH,
});We believe these two modes can be very handy to help the end users identify places, especially when the local labels are not using a latin charset. Here is how it looks like:

Custom Events and Map Lifecycle
Events
The ready event
The ready event happens just after the load event but waits until all the controls managed by the Map constructor are dealt with, some having an asynchronous logic to set up.
Since the ready event waits until all the basic controls are nicely positioned, it is safer to use ready than load if you plan to add other custom controls with the .addControl() method.
This event works exactly the same way as load and you can safely replace those by "ready". Here is a usage example:
const map = new maptilersdk.Map({
container: "map-container",
});
map.on("ready", (evt) => {
const terrainControl = new maptilersdk.MaptilerTerrainControl();
map.addControl(terrainControl);
});The loadWithTerrain event
The loadWithTerrain event is triggered only once in a Map instance lifecycle, when both the ready event and the terrain event with non-null terrain are fired.
Why a new event?
When a map is instantiated with the option terrain: true, then MapTiler terrain is directly added to it and some animation functions such as .flyTo() or .easeTo() if started straight after the map initialization will actually need to wait a few milliseconds that the terrain is properly initialized before running.
Relying on the ready or load event to run an animation with a map with terrain may fail in some cases for this reason, and this is why waiting for loadWithTerrain is safer in this particular situation.
Lifecycle Methods
The events load, ready and loadWithTerrain are both called at most once and require a callback function to add more elements such as markers, layers, popups and data sources. Even though MapTiler SDK fully supports this logic, we have also included a promise logic to provide a more linear and less nested way to wait for a Map instance to be usable. Let's compare the two ways:
- Classic: with a callback on the
loadevent:
function init() {
const map = new Map({
container,
center: [2.34804, 48.85439], // Paris, France
zoom: 14,
});
// We wait for the event.
// Once triggered, the callback is ran in its own scope.
map.on("load", (evt) => {
// Adding a data source
map.addSource("my-gps-track-source", {
type: "geojson",
data: "https://example.com/some-gps-track.geojson",
});
});
}- Modern: with a promise returned by the method
.onLoadAsync(), used in anasyncfunction:
async function init() {
const map = new Map({
container,
center: [2.34804, 48.85439], // Paris, France
zoom: 14,
});
// We wait for the promise to resolve.
// Once triggered, the rest of the init function runs
await map.onLoadAsync();
// Adding a data source
map.addSource("my-gps-track-source", {
type: "geojson",
data: "https://example.com/some-gps-track.geojson",
});
}We deployed exactly the same logic for the loadWithTerrain event. Let's see how the two ways compare.
- Classic: with a callback on the
loadWithTerrainevent:
function init() {
const map = new Map({
container,
center: [2.34804, 48.85439], // Paris, France
zoom: 14,
terrain: true,
});
// We wait for the event.
// Once triggered, the callback is ran in its own scope.
map.on("loadWithTerrain", (evt) => {
// make an animation
map.flyTo({
center: [-0.09956, 51.50509], // London, UK
zoom: 12.5,
});
});
}- Modern: with a promise returned by the method
.onLoadWithTerrainAsync(), used in anasyncfunction:
async function init() {
const map = new Map({
container,
center: [2.34804, 48.85439], // Paris, France
zoom: 14,
terrain: true,
});
// We wait for the promise to resolve.
// Once triggered, the rest of the init function runs
await map.onLoadWithTerrainAsync();
// make an animation
map.flyTo({
center: [-0.09956, 51.50509], // London, UK
zoom: 12.5,
});
}And finally, the lifecycle method corresponding to the ready event:
- Classic: with a callback on the
readyevent:
function init() {
const map = new Map({
container,
center: [2.34804, 48.85439], // Paris, France
zoom: 14,
});
// We wait for the event.
// Once triggered, the callback is ran in its own scope.
map.on("ready", (evt) => {
// Adding a data source
map.addSource("my-gps-track-source", {
type: "geojson",
data: "https://example.com/some-gps-track.geojson",
});
});
}- Modern: with a promise returned by the method
.onReadyAsync(), used in anasyncfunction:
async function init() {
const map = new Map({
container,
center: [2.34804, 48.85439], // Paris, France
zoom: 14,
});
// We wait for the promise to resolve.
// Once triggered, the rest of the init function runs
await map.onReadyAsync();
// Adding a data source
map.addSource("my-gps-track-source", {
type: "geojson",
data: "https://example.com/some-gps-track.geojson",
});
}We believe that the promise approach is better because it does not nest scopes and will allow for a linear non-nested stream of execution. It also corresponds to more modern development standards.
📣 Note: Generally speaking, promises are not a go to replacement for all event+callback and are suitable only for events that are called only once in the lifecycle of a Map instance. This is the reason why we have decided to provide a promise equivalent only for the
load,readyandloadWithTerrainevents but not for events that may be called multiple time such as interaction events.
The webglContextLost event
The map is rendered with WebGL, that leverages the GPU to provide high-performance graphics. In some cases, the host machine, operating system or the graphics driver, can decide that continuing to run such high performance graphics is unsustainable, and will abort the process. This is called a "WebGL context loss". Such situation happens when the resources are running low or when multiple browser tabs are competing to access graphics memory.
The best course of action in such situation varies from an app to another. Sometimes a page refresh is the best thing to do, in other cases, instantiating a new Map dynamically at application level is more appropriate because it hides a technical failure to the end user. The event webglContextLost is exposed so that the most appropriate scenario can be implemented at application level.
Here is how to respond to a WebGL context loss with a simple page refresh:
// Init the map
const map = new maptilersdk.Map({
container: "map-container",
hash: true,
});
// Refresh the page if context is lost.
// Since `hash` is true, the location will be the same as before
map.on("webglContextLost", (e) => {
location.reload();
});Color Ramps
A color ramp is a color gradient defined in a specific interval, for instance in [0, 1], and for any value within this interval will retrieve a color. They are defined by at least a color at each bound and usually additional colors within the range.
Color ramps are super useful to represent numerical data in a visual way: the temperature, the population density, the average commute time, etc.
The SDK includes many built-in ready-to-use color ramps as well as extra logic to manipulate them and create new ones, here is the full list:

To use an already existing color ramp and access some of its values:
import { ColorRampCollection } from "@maptiler/sdk";
// The TURBO color ramp, just like all the built-ins, is defined in [0, 1],
// but we can rescale it to fit the range of temperature [-18, 38]°C (equivalent to [0, 100]F)
// and this actually creates a clone of the original TURBO
const temperatureTurbo = ColorRampCollection.TURBO.scale(-18, 38);
// What's the color at 0°C (or 32F) ?
const zeroColor = temperatureTurbo.getColor(0);
// The color is an array: [45, 218, 189, 255]
// Alternatively, we can ask for the hex color:
const zeroColorHex = temperatureTurbo.getColorHex(0);
// The color is a string: "#2ddabdff"Creating a new one consists of defining all the colors for each color stops. The values can be in the range of interest and do not have to be in [0, 1]. For example, let's recreate a Viridis color ramp but with a range going from 0 to 100:
import { ColorRamp } from "@maptiler/sdk";
const myCustomRamp = new ColorRamp({
stops: [
{ value: 0, color: [68, 1, 84] },
{ value: 13, color: [71, 44, 122] },
{ value: 25, color: [59, 81, 139] },
{ value: 38, color: [44, 113, 142] },
{ value: 5, color: [33, 144, 141] },
{ value: 63, color: [39, 173, 129] },
{ value: 75, color: [92, 200, 99] },
{ value: 88, color: [170, 220, 50] },
{ value: 100, color: [253, 231, 37] },
],
});When defining a new ramp, the colors can be an RGB array ([number, number, number]) or an RGBA array ([number, number, number, number]).
Many methods are available on color ramps, such as getting the <canvas> element of it, rescaling it, flipping it or resampling it in a non-linear way. Read more on our reference page and have a look at our examples to see how they work.
Camera routes and animations
The SDK comes with several classes to help with animations, particularly route animations.
See demos/11-animated-routes.html for examples.
See demos/12-maptiler-animation.html for examples.
🧩 MaptilerAnimation
MaptilerAnimation is a utility class for smoothly animating between keyframes using custom easing and playback control. It supports event-based hooks for frame updates and completion, and works well within rendering loops or UI transitions.
🚀 Usage
// linearly animated between values
const animation = new MaptilerAnimation({
keyframes: [
// `props` are interpolated across the duration
{ delta: 0, props: { lon: -7.445, } },
// `userData` can hold any type of custom data to pass with the keyframe
{ delta: 0.5, userData: { mydata: "whoa!" } },
{ delta: 1, props: { lon: -7.473 } }
],
duration: 1000, // 1 second
iterations: Infinity // loop forever
});
const marker = new Marker().setLngLat(
new LngLat(
-7.449346225791231,
39.399728941536836,
)
).addTo(map);
// TimeUpdate is fired every frame
animation.addEventListener(AnimationEventTypes.TimeUpdate, (e) => {
marker.setLngLat(
new LngLat(
e.props.lon,
39.399728941536836,
)
)
})
// fired when the keyframe changes
animation.addEventListener(AnimationEventTypes.Keyframe, ({ userData }) => {
console.log(userData.mydata) // "whoa!"
});
animation.play();
// eased between values
const animation = new MaptilerAnimation({
keyframes: [
// `props` are interpolated across the duration
{ delta: 0, easing: EasingFunctionName.ElasticInOut, props: { lon: -7.445, } },
{ delta: 1, props: { lon: -7.455 } }
],
duration: 1000, // 1 second
iterations: Infinity // loop forever
});
🗺️ AnimatedRouteLayer
AnimatedRouteLayer is custom layer that animates a path or route on the map based on keyframes or GeoJSON data. It supports animated line styling and camera following, making it ideal for visualizing routes, playback tracks, or timeline-based geographic events.
Note: At present, to avoid problems arising from the camera being manipulated by two animations at any one time, there can only ever be one instance of AnimatedRouteLayer on the map at any time. This API may change in the future, but at present you must remove each instance of AnimatedRouteLayer from the map before adding another.
✨ Features
- Animate a path using keyframes or GeoJSON data
- Optional animated stroke styles to indicate progress
- Camera movement smoothing, following along the route
- Configurable duration, easing, delay, and iterations via geojson properties
- Event-based lifecycle hooks for adaptability.
- Optional manual frame advancement (e.g., for scrubbing or syncing with map events, scroll etc etc)
🚀 Basic Usage
const myGeoJSONSource = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[-74.0060, 40.7128],
[-73.9352, 40.7306],
[-73.9851, 40.7580]
]
},
"properties": {
"@duration": 5000, // animation params are prepended with '@'
"@iterations": 3,
"@delay": 1000,
"@autoplay": true,
"bearing": [
40,
30,
10,
10,
20,
40,
]
}
}
]
}
map.addSource("my-geojson-source", {
type: "geojson",
data: myGeoJSONSource,
});
const animatedRoute = new AnimatedRouteLayer({
source: {
// assumes that the source is already added to the map with the given layer ID
id: "my-geojson-source", // the name of the source
layerID: "route-layer", // the name of the layer
},
// OR
keyframes: [], // an array of keyframes
duration: 5000,
pathStrokeAnimation: {
// will only be applied to LineString GeoJSON types
activeColor: [0, 128, 0, 1], // color of the line that has already been traversed
inactiveColor: [128, 128, 128, 0.5],
},
cameraAnimation: {
follow: true, // should the camera follow the route?
pathSmoothing: {
resolution: 20, // the resolution of the smoothness
epsilon: 10, // how much the path is simplified before smoothing
},
},
autoplay: true,
});
// Add to map
map.addLayer(animatedRoute);
// Playback controls
animatedRoute.play();
animatedRoute.pause();For a full example of how to use this, look at the example
Vector Layer Helpers
Let's make vector layers easy! Originally, you'd have to add a source and then proceed to the styling of your layer, which can be tricky because there are a lot of paint and layout options and they vary a lot from one type of layer to another. But we have helpers for this! 🖋️

Shared logic
Helpers come with a lot of built-in defaults and some fail-proof logic that makes creating vector layers much easier! As a result, a dataset can be displayed in one call, creating both the datasource and the layer(s) in one go!
Depending on the type of feature to add (point, polyline, polygon or heatmap), a different helper function needs to be used, but datasource could contain mixed types of feature and the helper will only display a specific type.
All the helpers are made available under the helpers object. If you are using ES Modules, this is how you access them:
import { Map, helpers } from "@maptiler/sdk";If you are using the UMD bundle of the SDK, for example from our CDN, you will find the helpers with:
maptilersdk.helpers;Example: we have a geoJSON file that contains both polygons and point and we use it as the data property on the helpers.addPoint(map, { options }), this will only add the points.
In addition to easy styling, the helpers' datasource can be:
- a URL to a geoJSON file or its string content
- a URL to a GPX or KML file (only for the polyline helper) or its string content
- a UUID of a MapTiler dataset
Multiple Layers
The key design principle of these vector layer helpers is it's easy to make what you want, which is very different from making MapLibre easier to use.
For example, to create a road with an outline, one must draw two layers: a wider base layer and a narrower top layer, fueled by the same polyline data. This requires ordering the layers properly and computing not the width of the outline, but rather the width of the polyline underneath so that it outgrows the top road layer of the desired number of pixels.
With the polyline helper, you just say if you want an outline and specify its size (or even a zoom-dependent size) and everything is handled for you. As a result, calling the method helpers.addPolyline will return an object with multiple IDs: the ID of the top/main layer, the ID of the outline layer (could be null) and the ID of the data source. This makes further layer and source manipulation possible.
Input
The vector layer helper also shares some I/O logic: each of them can take many options but a subset of them is common across all the helpers:
/**
* A geojson Feature collection or a URL to a geojson or the UUID of a MapTiler dataset.
*/
data: FeatureCollection | string;
/**
* ID to give to the layer.
* If not provided, an auto-generated ID of the for "maptiler-layer-xxxxxx" will be auto-generated,
* with "xxxxxx" being a random string.
*/
layerId?: string;
/**
* ID to give to the geojson source.
* If not provided, an auto-generated ID of the for "maptiler-source-xxxxxx" will be auto-generated,
* with "xxxxxx" being a random string.
*/
sourceId?: string;
/**
* The ID of an existing layer to insert the new layer before, resulting in the new layer appearing
* visually beneath the existing layer. If this argument is not specified, the layer will be appended
* to the end of the layers array and appear visually above all other layers.
*/
beforeId?: string;
/**
* Zoom level at which it starts to show.
* Default: `0`
*/
minzoom?: number;
/**
* Zoom level after which it no longer show.
* Default: `22`
*/
maxzoom?: number;Polyline Layer Helper
The method helpers.addPolyline is not only compatible with the traditional GeoJSON source but also with GPX and KML files and the .data options can be a MapTiler dataset UUID and will be resolved automatically.
here is the minimal usage, with the default line width and a random color (within a selected list):
helpers.addPolyline(map, {
// A URL, relative or absolute
data: "some-trace.geojson",
});
We can add many options, such as specific color, a custom width or a dash pattern, this time sourcing the data from MapTiler, using the UUID of a dataset:
helpers.addPolyline(map, {
data: "74003ba7-215a-4b7e-8e26-5bbe3aa70b05",
lineColor: "#FF6666",
lineWidth: 4,
lineDashArray: "____ _ ",
lineCap: "butt",
});
As you can see, we've come up with a fun and easy way to create dash arrays, just use underscores and white spaces and this pattern will repeat!
Adding an outline is also pretty straightforward:
helpers.addPolyline(map, {
data: "74003ba7-215a-4b7e-8e26-5bbe3aa70b05",
lineColor: "#880000",
outline: true,
});
Endless possibilities, what about a glowing wire?
helpers.addPolyline(map, {
data: "74003ba7-215a-4b7e-8e26-5bbe3aa70b05",
lineColor: "#fff",
lineWidth: 1,
outline: true,
outlineColor: "#ca57ff",
outlineWidth: 10,
outlineBlur: 10,
outlineOpacity: 0.5,
});
All the other options are documented on our reference page and more examples are available here.
Polygon Layer Helper
The polygon helper makes it easy to create vector layers that contain polygons, whether they are multipolygons, holedpolygons or just simple polygons. Whenever it's possible and it makes sense, we use the same terminology across the different helpers.
Here is a minimalist example, with a half-transparent polygon of Switzerland, from a local file:
helpers.addPolygon(map, {
data: "switzerland.geojson",
fillOpacity: 0.5,
});Again, if no color is specified, a random one from a list is being picked:

Plenty of options are available to create interesting thematic visualizations:
helpers.addPolygon(map, {
data: "switzerland.geojson",
pattern: "cheese512.png",
outline: true,
outlineWidth: 3,
outlineColor: "white",
outlineDashArray: "_ ",
fillOpacity: 0.7,
});
All the other options are documented on our reference page and more examples are available [here](https://docs.maptiler.com/sdk-js/examples/?q=po
