---
title: "Add a custom control programmatically | JavaScript maps SDK"
description: "Programmatically add custom controls for dynamic map logic integration."
---

> For AI agents: a documentation index is available at [/llms.txt](https://docs.maptiler.com/llms.txt). Markdown versions of all pages are available by appending .md to any URL.

# Add a custom control programmatically

Adding a custom control programmatically allows developers to have more control is ideal for applications that require dynamic logic, event-driven behaviour, or a deeper integration with a framework like React.

Programmatic controls allow developers to register custom control elements manually by calling `map.addControl()` and providing a control implementation. Custom controls are instantiated using the `MaptilerCustomControl` class. The element that should be used can be provided either as the **element itself**, or as its **CSS selector**.

### NPM module setup

```bash
npm install --save @maptiler/sdk bootstrap
```

#### main.js

```js
import { Map, MapStyle, config, MaptilerCustomControl } from '@maptiler/sdk';
import '@maptiler/sdk/dist/maptiler-sdk.css';
import 'bootstrap/dist/css/bootstrap.min.css';

config.apiKey = 'YOUR_MAPTILER_API_KEY_HERE';
const map = new Map({
  container: 'map', // container's id or the HTML element to render the map
  style: MapStyle.STREETS,
  navigationControl: false,
  geolocateControl: false,
  terrainExaggeration: 4,
});

//custom navigation controls
const compassControlElement = document.querySelector(".compass-control");
const navigationControl = new MaptilerCustomControl(
  ".navigation-controls",
  (map, _, event) => {
    if ((event.target).closest(".zoom-in-control")) {
      map.zoomIn();
    }
  },
  (map) => {
    (compassControlElement.firstElementChild).style.transform = `rotateX(${String(map.getPitch())}deg) rotateZ(${String(-map.getBearing())}deg)`;
  },
);
map.addControl(navigationControl);
document.querySelector(".zoom-out-control")?.addEventListener("click", () => map.zoomOut());
compassControlElement.addEventListener("click", () => map.resetNorthPitch());

//custom projection control
function toggleProjection() {
  if (map.isGlobeProjection()) {
    map.setProjection({ type: "mercator" });
  } else {
    map.setProjection({ type: "globe" });
  }
}
const projectionControlElement = document.querySelector(".projection-control");
const projectionControl = new MaptilerCustomControl(projectionControlElement, toggleProjection, (map, element) => {
  element.firstElementChild.textContent = map.isGlobeProjection() ? "map" : "globe";
  element.title = map.isGlobeProjection() ? "Switch to Mercator projection" : "Switch to Globe projection";
});
map.addControl(projectionControl, "top-left");

//custom terrain control
function toggleTerrain() {
  if (map.hasTerrain()) {
    map.disableTerrain();
  } else {
    map.enableTerrain();
  }
}
const terrainControlElement = document.createElement("div");
terrainControlElement.className = "btn btn-success m-2 terrain-control";
const terrainControlIcon = document.createElement("span");
terrainControlIcon.textContent = "terrain";
terrainControlIcon.classList.add("material-symbols-outlined");
terrainControlElement.appendChild(terrainControlIcon);
const terrainControl = new MaptilerCustomControl(terrainControlElement, toggleTerrain, (map) => {
  terrainControlIcon.classList.toggle("icon-fill", map.hasTerrain());
  terrainControlElement.title = map.hasTerrain() ? "Turn off Terrain" : "Turn on Terrain";
});
map.addControl(terrainControl, "top-left");

//custom control (center map)
const home = { lng: 15.4, lat: 49.8 };
map.addControl(
  new MaptilerCustomControl(
    ".home-button",
    (map) => {
      map.easeTo({ center: home, zoom: 7 });
    },
    (map, element) => {
      const center = map.getCenter();
      const zoom = map.getZoom();
      (element.firstElementChild).classList.toggle(
        "icon-fill",
        Math.abs(center.lng - home.lng) < Math.max(0, zoom - 6.8) * 1.5 && Math.abs(center.lat - home.lat) < Math.max(0, zoom - 6.8) / 2,
      );
    },
  ),
  "bottom-right",
);
```

#### index.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="robots" content="noindex">
  <title>Add a custom control programmatically | NPM example</title>
  <link rel="stylesheet" href="./style.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:FILL@0..1">
</head>
<body>
  <div id="map"></div>
  <div hidden>
    <div class="btn-group-vertical m-2 navigation-controls">
      <button class="btn btn-light zoom-in-control">
        <span class="material-symbols-outlined">zoom_in</span>
      </button>
      <button class="btn btn-light zoom-out-control">
        <span class="material-symbols-outlined">zoom_out</span>
      </button>
      <button class="btn btn-light compass-control">
        <span class="material-symbols-outlined">navigation</span>
      </button>
    </div>
    <button class="btn btn-primary m-2 projection-control">
      <span class="material-symbols-outlined">globe</span>
    </button>
    <button class="btn btn-danger m-2 float-end home-button">
      <span class="material-symbols-outlined">home</span>
    </button>
  </div>
  <script type="module" src="./main.js"></script>
</body>
</html>
```

#### style.css

```css
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
.maplibregl-control-container .btn {
  pointer-events: auto;
  display: block;
}

.material-symbols-outlined {
  transition: font-variation-settings 1s ease-in-out;
  font-variation-settings: 'FILL' 0;
}
.icon-fill {
  font-variation-settings: 'FILL' 1;
}
```

### Basic JavaScript setup

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>MapTiler custom controls</title>
  <script src="https://cdn.maptiler.com/maptiler-sdk-js/v4.2.0/maptiler-sdk.umd.min.js"></script>
  <link href="https://cdn.maptiler.com/maptiler-sdk-js/v4.2.0/maptiler-sdk.css" rel="stylesheet" />
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:FILL@0..1">
  <style>
    body { margin: 0; padding: 0; }
    #map { position: absolute; top: 0; bottom: 0; width: 100%; }
    .maplibregl-control-container .btn {
      pointer-events: auto;
      display: block;
    }

    .material-symbols-outlined {
      transition: font-variation-settings 1s ease-in-out;
      font-variation-settings: 'FILL' 0;
    }
    .icon-fill {
      font-variation-settings: 'FILL' 1;
    }
  </style>
</head>
<body>
  <div id="map"></div>
  <div hidden>
    <div class="btn-group-vertical m-2 navigation-controls">
      <button class="btn btn-light zoom-in-control">
        <span class="material-symbols-outlined">zoom_in</span>
      </button>
      <button class="btn btn-light zoom-out-control">
        <span class="material-symbols-outlined">zoom_out</span>
      </button>
      <button class="btn btn-light compass-control">
        <span class="material-symbols-outlined">navigation</span>
      </button>
    </div>
    <button class="btn btn-primary m-2 projection-control">
      <span class="material-symbols-outlined">globe</span>
    </button>
    <button class="btn btn-danger m-2 float-end home-button">
      <span class="material-symbols-outlined">home</span>
    </button>
  </div>
  <script>
    maptilersdk.config.apiKey = 'YOUR_MAPTILER_API_KEY_HERE';
    const map = new maptilersdk.Map({
      container: 'map', // container's id or the HTML element to render the map
      style: maptilersdk.MapStyle.STREETS,
      navigationControl: false,
      geolocateControl: false,
      terrainExaggeration: 4,
    });
    
    //custom navigation controls
    const compassControlElement = document.querySelector(".compass-control");
    const navigationControl = new maptilersdk.MaptilerCustomControl(
      ".navigation-controls",
      (map, _, event) => {
        if ((event.target).closest(".zoom-in-control")) {
          map.zoomIn();
        }
      },
      (map) => {
        (compassControlElement.firstElementChild).style.transform = `rotateX(${String(map.getPitch())}deg) rotateZ(${String(-map.getBearing())}deg)`;
      },
    );
    map.addControl(navigationControl);
    document.querySelector(".zoom-out-control")?.addEventListener("click", () => map.zoomOut());
    compassControlElement.addEventListener("click", () => map.resetNorthPitch());

    //custom projection control
    function toggleProjection() {
      if (map.isGlobeProjection()) {
        map.setProjection({ type: "mercator" });
      } else {
        map.setProjection({ type: "globe" });
      }
    }
    const projectionControlElement = document.querySelector(".projection-control");
    const projectionControl = new maptilersdk.MaptilerCustomControl(projectionControlElement, toggleProjection, (map, element) => {
      element.firstElementChild.textContent = map.isGlobeProjection() ? "map" : "globe";
      element.title = map.isGlobeProjection() ? "Switch to Mercator projection" : "Switch to Globe projection";
    });
    map.addControl(projectionControl, "top-left");

    //custom terrain control
    function toggleTerrain() {
      if (map.hasTerrain()) {
        map.disableTerrain();
      } else {
        map.enableTerrain();
      }
    }
    const terrainControlElement = document.createElement("div");
    terrainControlElement.className = "btn btn-success m-2 terrain-control";
    const terrainControlIcon = document.createElement("span");
    terrainControlIcon.textContent = "terrain";
    terrainControlIcon.classList.add("material-symbols-outlined");
    terrainControlElement.appendChild(terrainControlIcon);
    const terrainControl = new maptilersdk.MaptilerCustomControl(terrainControlElement, toggleTerrain, (map) => {
      terrainControlIcon.classList.toggle("icon-fill", map.hasTerrain());
      terrainControlElement.title = map.hasTerrain() ? "Turn off Terrain" : "Turn on Terrain";
    });
    map.addControl(terrainControl, "top-left");

    //custom control (center map)
    const home = { lng: 15.4, lat: 49.8 };
    map.addControl(
      new maptilersdk.MaptilerCustomControl(
        ".home-button",
        (map) => {
          map.easeTo({ center: home, zoom: 7 });
        },
        (map, element) => {
          const center = map.getCenter();
          const zoom = map.getZoom();
          (element.firstElementChild).classList.toggle(
            "icon-fill",
            Math.abs(center.lng - home.lng) < Math.max(0, zoom - 6.8) * 1.5 && Math.abs(center.lat - home.lat) < Math.max(0, zoom - 6.8) / 2,
          );
        },
      ),
      "bottom-right",
    );
  </script>
</body>
</html>
```

## Learn more

Check out the [MaptilerCustomControl API reference](https://docs.maptiler.com/sdk-js/api/controls/#maptilercustomcontrol) to learn all the options to add custom controls to your map interface.

## Related examples

### [Custom control declarative](https://docs.maptiler.com/sdk-js/examples/custom-controls-declarative/)

Examples

Add interactive custom controls to maps using declarative HTML.

### [Add custom zoom control](https://docs.maptiler.com/sdk-js/examples/custom-control/)

Examples

Add custom zoom level control to your map interface.

### [Hide map navigation controls](https://docs.maptiler.com/sdk-js/examples/navigation/)

Tutorials

Hide the map navigation and zoom controls.

### [Scale control](https://docs.maptiler.com/sdk-js/examples/control-scale/)

Tutorials

Learn to add and display map scale control.
