Skip to content

Add a Style Switcher to MapLibre GL JS

@maptoolkit/maplibre-style-control is a MapLibre GL JS control that gives your users a style switcher: a panel of thumbnails, one per basemap, that swaps the map style when clicked. It is open source under BSD 3-Clause and maintained at github.com/maptoolkit/maplibre-style-control.

The demo below uses the seven Maptoolkit styles, each loaded from a keyed style URL.

Open the full-screen demo

Install

npm install @maptoolkit/maplibre-style-control maplibre-gl
import maplibregl from "maplibre-gl";
import { StyleControl } from "@maptoolkit/maplibre-style-control";
import "@maptoolkit/maplibre-style-control/style.css";

const map = new maplibregl.Map({ container: "map", style, center, zoom });
map.addControl(new StyleControl());

The stylesheet is a separate import and the control renders unstyled without it.

It also ships a UMD build, so it works from a script tag with no bundler. The global is MaplibreStyleControl:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-style-control@1.0.2/dist/maplibre-style-control.css" />
<script src="https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-style-control@1.0.2/dist/maplibre-style-control.js"></script>
<script>
  map.addControl(new MaplibreStyleControl.StyleControl());
</script>

Requires maplibre-gl 3.0.0 or later as a peer dependency.

Point it at your own styles

This is the part to get right. The plugin’s built-in styles are the seven community styles from styles.maptoolkit.org, which take no API key and are served under the Maptoolkit Community License. Adding the control with its defaults therefore replaces whatever style your map was built with, on the first render, and moves the map onto the community service.

If you are a Maptoolkit customer, pass your own styles array so the map stays on the styles your key pays for:

const API_KEY = "YOUR_API_KEY";
const style = (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: style(n),
  image: `https://staticmap.maptoolkit.net?maptype=maptoolkit-maptoolkit.${n}&size=120x120&center=47.310897,12.805988&zoom=12.1`,
}));

map.addControl(new StyleControl({ styles, active: "Summer" }), "top-left");

The thumbnails above come from the Static Maps API, so they show the real cartography at a location you choose rather than a generic sample. Any image URL works.

The control’s default position is bottom-left, which is also where the Maptoolkit logo goes. Left at the default the two overlap, and attribution that is covered does not count as displayed.

An open panel also overlaps anything else in its own corner, including the zoom control, and the panel has no z-index of its own, so whichever control was added last paints on top. Give the switcher a corner to itself:

map.addControl(new StyleControl(), "top-left");

The panel opens from whichever corner you choose, so all four work. With the logo bottom-left and the zoom control top-right, top-left is the corner that stays free.

Options

OptionTypeDefaultDescription
stylesStyleDefSpecification[]the seven community stylesStyles offered in the panel.
activestring"Summer"id of the style selected on load.

StyleDefSpecification:

FieldTypeDescription
idstringUnique id. Also the label, and the key used for translation.
valuestring | StyleSpecificationA style URL, or an inline style object.
imagestring?Thumbnail shown in the panel.

To add to the built-in list rather than replace it, spread defaultStyleControlOptions:

import { StyleControl, defaultStyleControlOptions } from "@maptoolkit/maplibre-style-control";

new StyleControl({
  styles: [...defaultStyleControlOptions.styles, { id: "Custom", value: "https://example.com/style.json" }],
});

Note that this keeps the community styles in the list, so a customer map would offer both keyed and keyless styles side by side.

A custom id needs a matching label

The id doubles as the translation key, and the control only registers labels for its seven built-in ids. Any other id, including a lower-case spelling of a built-in one, throws Missing UI string 'StyleControl.Style.<id>' and the control does not render at all. The map is left with no switcher and an error in the console.

Register the label in the map’s locale and any id works:

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

map.addControl(new StyleControl({ styles: [{ id: "Brand", value: "..." }] }), "top-left");

The seven ids that need no entry are Summer, Winter, Light, Dark, Cycling, Hiking and Street, spelled exactly like that.

Methods

MethodDescription
setStyle(styleId)Switches to the style with that id, as if it had been clicked. Fires style.set.
open()Opens the panel.
close()Closes the panel.

setStyle is attached when the control is added to a map, so it is undefined until after map.addControl(control).

Events

The control extends MapLibre’s Evented, so it is subscribed to like the map itself.

EventPayloadFired when
style.set{ style: StyleDefSpecification }The active style changes.
const control = new StyleControl();
map.addControl(control);
control.on("style.set", (e) => console.log(e.style.id));

It fires on the initial render as well as on user clicks, so a handler that persists the choice will see one event before the user has touched anything.

Restoring your layers after a switch

Changing the style throws away every source and layer on the map, including ones you added. Re-add yours when 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" });
    }
  });
});

Theming

Appearance is driven by CSS custom properties on .maplibre-style-control, so it can be themed from your own stylesheet without touching the plugin:

.maplibre-style-control {
  --style-control-radius: 4px;
  --style-control-color-primary: #0074d9;
}

The full list of --style-control-* variables is in src/style.css in the repository.

Using it with Maps JS

This plugin works on a Maps JS map too, since Maps JS extends MapLibre GL JS and the plugin is a plain IControl. Add a Style Switcher in Maptoolkit Maps JS is the worked example, including the keyed-style list and the alias below.

Note that Maps JS exports a class of its own called StyleControl. It is a different control that happens to share the name, and it is not recommended for new work, so with both loaded keep them apart: MaplibreStyleControl.StyleControl is this plugin, maptoolkit.StyleControl is the other one.

One thing differs:

  • From a script tag it needs a maplibregl global. The UMD build expects window.maplibregl and Maps JS provides window.maptoolkit, so without an alias the control fails to construct with Cannot read properties of undefined (reading 'Evented'). Set window.maplibregl = window.maptoolkit; after the Maps JS script and before the plugin.

Next steps