Skip to content
Add a Style Switcher

Add a Style Switcher in MapLibre GL JS

A style switcher lets the reader pick the basemap: a panel of thumbnails, one per style, that changes the map style when clicked. This example uses the Style Control plugin on MapLibre GL JS 5.6.1, loaded from a CDN without a bundler.

Open the full-screen demo

import { LogoControl } from "https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-logo-control@1.1.2/dist/maplibre-logo-control.js";
    import { StyleControl } from "https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-style-control@1.1.2/dist/maplibre-style-control.js";

    const API_KEY = "YOUR_API_KEY";
    const styleUrl = (name) =>
      `https://styles.maptoolkit.net/maptoolkit/maptoolkit.${name}.json?api_key=${API_KEY}`;
    const thumb = (name) =>
      `https://staticmap.maptoolkit.net?maptype=maptoolkit-maptoolkit.${name}&size=120x120&center=47.27574,11.39085&zoom=12`;

    const styles = ["summer", "winter", "hiking", "cycling", "street", "light", "dark"].map((n) => ({
      id: n[0].toUpperCase() + n.slice(1),
      value: styleUrl(n),
      image: thumb(n),
    }));

    const map = new maplibregl.Map({
      container: "map",
      style: styleUrl("summer"),
      center: [11.39085, 47.27574],
      zoom: 12,
      attributionControl: { compact: false },
    });

    const control = new StyleControl({ styles, active: "Summer" });
    map.addControl(control, "top-left");
    map.addControl(new maplibregl.NavigationControl(), "top-right");

    control.on("style.set", (e) => console.log("style.set", e.style.id));
    map.addControl(new LogoControl({ variant: "com" }));
<!DOCTYPE html>
<html lang="en">
<head>
  <title>Add a Style Switcher - MapLibre GL JS</title>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@5.6.1/dist/maplibre-gl.css" />
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-logo-control@1.1.2/dist/maplibre-logo-control.css" />
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-style-control@1.1.2/dist/maplibre-style-control.css" />
  <script src="https://cdn.jsdelivr.net/npm/maplibre-gl@5.6.1/dist/maplibre-gl.js"></script>
  <!-- The plugin is an ES module that imports from "maplibre-gl". MapLibre 5 ships no module
       build, so this import map points "maplibre-gl" to a shim that re-exports the global. -->
  <script type="importmap">
    {
      "imports": {
        "maplibre-gl": "data:text/javascript,const m=window.maplibregl;export default m;export const{Evented,LngLatBounds,Map,Marker}=m;"
      }
    }
  </script>
  <style>
    html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
    #map { width: 100%; height: 100%; }
  </style>
</head>
<body>
  <div id="map"></div>
  <script type="module">
    import { LogoControl } from "https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-logo-control@1.1.2/dist/maplibre-logo-control.js";
    import { StyleControl } from "https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-style-control@1.1.2/dist/maplibre-style-control.js";

    const API_KEY = "YOUR_API_KEY";
    const styleUrl = (name) =>
      `https://styles.maptoolkit.net/maptoolkit/maptoolkit.${name}.json?api_key=${API_KEY}`;
    const thumb = (name) =>
      `https://staticmap.maptoolkit.net?maptype=maptoolkit-maptoolkit.${name}&size=120x120&center=47.27574,11.39085&zoom=12`;

    const styles = ["summer", "winter", "hiking", "cycling", "street", "light", "dark"].map((n) => ({
      id: n[0].toUpperCase() + n.slice(1),
      value: styleUrl(n),
      image: thumb(n),
    }));

    const map = new maplibregl.Map({
      container: "map",
      style: styleUrl("summer"),
      center: [11.39085, 47.27574],
      zoom: 12,
      attributionControl: { compact: false },
    });

    const control = new StyleControl({ styles, active: "Summer" });
    map.addControl(control, "top-left");
    map.addControl(new maplibregl.NavigationControl(), "top-right");

    control.on("style.set", (e) => console.log("style.set", e.style.id));
    map.addControl(new LogoControl({ variant: "com" }));
  </script>
</body>
</html>

Use the prompt below with any LLM to get the same result. Make sure the Maptoolkit MCP server is connected first — check out AI Integration & MCP to get started.

Use the Maptoolkit Connector. Create a MapLibre GL JS map centered on [11.39085, 47.27574] at zoom 12 with the @maptoolkit/maplibre-style-control plugin in the top-left corner that offers the seven Maptoolkit styles.

Load the plugin without a bundler

The plugin is an ES module that imports from maplibre-gl. MapLibre GL JS 6 ships an ES module build, so on version 6 an import map that points maplibre-gl to that build is enough. See the plugin page.

MapLibre GL JS 5 only ships a script build that creates the global maplibregl. To use the plugin with it, the import map points maplibre-gl to a small shim module that exports that global:

<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@5.6.1/dist/maplibre-gl.js"></script>
<script type="importmap">
  {
    "imports": {
      "maplibre-gl": "data:text/javascript,const m=window.maplibregl;export default m;export const{Evented,LngLatBounds,Map,Marker}=m;"
    }
  }
</script>

Keep three things in mind:

  • The import map must come before the first <script type="module"> on the page.
  • The shim lists every name that a Maptoolkit plugin imports from maplibre-gl. The same shim works for the Style, Logo, Isochrone and Legend controls.
  • Load the plugin from its dist/*.js file. jsDelivr’s +esm URLs resolve maplibre-gl on their own and load a second copy of MapLibre GL JS 6.

With npm and a bundler you do not need the shim. The plugin declares maplibre-gl 6 as a peer dependency, so on MapLibre GL JS 5 install it with --legacy-peer-deps:

npm install maplibre-gl@5.6.1 @maptoolkit/maplibre-style-control --legacy-peer-deps

Point it at your keyed styles

The plugin ships with the seven community styles from styles.maptoolkit.org, which take no API key. If you add the control with its defaults, it replaces the style your map was built with on the first render and moves the map onto the community service.

Pass your own styles array so the map stays on the styles your key pays for:

const styleUrl = (name) =>
  `https://styles.maptoolkit.net/maptoolkit/maptoolkit.${name}.json?api_key=${API_KEY}`;

const styles = ["summer", "winter", "hiking", "cycling", "street", "light", "dark"].map((n) => ({
  id: n[0].toUpperCase() + n.slice(1),
  value: styleUrl(n),
  image: `https://staticmap.maptoolkit.net?maptype=maptoolkit-maptoolkit.${n}&size=120x120&center=47.27574,11.39085&zoom=12`,
}));

Each entry has an id, a value that is a style URL or an inline style object, and an image for the thumbnail. The thumbnails above come from the Static Maps API, so they show the real map at a location you pick. Any image URL works.

The control shows the id as the label. For the seven built-in ids (Summer, Winter, Light, Dark, Cycling, Hiking and Street) it has its own labels. To show a different label for any id, add it to the map’s locale:

const map = new maplibregl.Map({
  container: "map",
  attributionControl: { compact: false },
  locale: { "StyleControl.Style.Brand": "Our brand style" },
});

Give it a corner of its own

The control defaults to bottom-left, and an open panel covers anything in the same corner. It has no z-index, so a control added after it is drawn on top: put it and the zoom buttons in one corner and the zoom buttons sit over the open panel. The demo uses top-left for the switcher and top-right for navigation.

map.addControl(control, "top-left");
map.addControl(new maplibregl.NavigationControl(), "top-right");

If your map shows the Maptoolkit logo in the bottom-left corner, keep the switcher away from that corner too. Attribution that is covered does not count as displayed.

React to a change

The control extends MapLibre’s Evented, so you subscribe to it the same way as to the map:

control.on("style.set", (e) => console.log(e.style.id));

It fires on the first render as well as on clicks, so a handler that saves the reader’s choice gets one event before anyone has used the control.

setStyle(id) switches the style from code, and open() and close() open and close the panel. setStyle is only available after addControl.

Add your layers again after a switch

Changing the style removes every source and layer on the map, including the ones you added. Add them again once the new style has loaded:

control.on("style.set", () => {
  map.once("styledata", () => {
    if (!map.getSource("route")) {
      map.addSource("route", { type: "geojson", data: routeGeoJSON });
      map.addLayer({ id: "route", type: "line", source: "route" });
    }
  });
});

Next steps